├── networks ├── .gitkeep ├── kovan-fork.json ├── mainnet-fork.json └── README.md ├── spec ├── certora │ ├── CErc20Delegator │ ├── contracts │ │ ├── MathCertora.sol │ │ ├── SimulationInterface.sol │ │ ├── CDaiDelegateCertora.sol │ │ ├── PriceOracleModel.sol │ │ ├── CEtherCertora.sol │ │ ├── MaximillionCertora.sol │ │ ├── InterestRateModelModel.sol │ │ ├── ComptrollerCertora.sol │ │ ├── CTokenCollateral.sol │ │ ├── TimelockCertora.sol │ │ ├── CErc20DelegateCertora.sol │ │ ├── CompCertora.sol │ │ ├── UnderlyingModelNonStandard.sol │ │ ├── mcd │ │ │ └── Lib.sol │ │ ├── UnderlyingModelWithFee.sol │ │ └── GovernorAlphaCertora.sol │ ├── cDAI │ │ └── mcd.cvl │ ├── Math │ │ └── int.cvl │ ├── Comp │ │ ├── search.cvl │ │ └── transfer.cvl │ ├── Maximillion │ │ └── maximillion.cvl │ ├── CErc20 │ │ ├── rounding.cvl │ │ └── exchangeRate.cvl │ └── Governor │ │ └── votes.cvl ├── sim │ ├── 0003-borrow-caps-patch │ │ ├── deploy.scen │ │ ├── hypothetical_upgrade_post_propose.scen │ │ ├── hypothetical_upgrade_post_deploy.scen │ │ └── hypothetical_upgrade.scen │ └── 0001-comp-distribution-patch │ │ └── deploy.scen └── scenario │ ├── ChangeDelegate.scen │ ├── ReEntry.scen │ ├── SetComptroller.scen │ ├── CTokenAdmin.scen │ ├── BorrowEth.scen │ ├── Withdraw.scen.old │ ├── Governor │ └── Defeat.scen │ ├── Seize.scen │ ├── BorrowWBTC.scen │ ├── Supply.scen.old │ └── PriceOracleProxy.scen ├── .gitattributes ├── .soliumignore ├── tests ├── Jest.js ├── Contracts │ ├── TetherInterface.sol │ ├── FalseMarker.sol │ ├── MathHelpers.sol │ ├── GovernorAlphaHarness.sol │ ├── ComptrollerScenarioG2.sol │ ├── FixedPriceOracle.sol │ ├── Counter.sol │ ├── ComptrollerScenarioG4.sol │ ├── Const.sol │ ├── Structs.sol │ ├── MockMCD.sol │ ├── TimelockHarness.sol │ ├── ComptrollerScenario.sol │ ├── CompHarness.sol │ ├── InterestRateModelHarness.sol │ ├── EvilToken.sol │ ├── Fauceteer.sol │ ├── ComptrollerScenarioG1.sol │ ├── FeeToken.sol │ ├── MockSushiBar.sol │ ├── ComptrollerScenarioG3.sol │ └── MockAggregator.sol ├── Utils │ ├── JS.js │ └── InfuraProxy.js ├── Tokens │ ├── registerAndUnregisterCollateral.js │ ├── safeTokenTest.js │ ├── setComptrollerTest.js │ └── transferTest.js ├── Governance │ └── CompScenarioTest.js ├── Errors.js ├── CompilerTest.js ├── Flywheel │ └── GasTest.js └── MaximillionTest.js ├── script ├── README.md ├── lint ├── verify ├── coverage ├── compile ├── test ├── saddle │ ├── contractSizer.js │ ├── matchToken.js │ ├── verifyToken.js │ └── deployToken.js ├── build_scenarios └── scen │ ├── scriptFlywheel.scen │ └── deploy.scen ├── reporterConfig.json ├── scenario ├── src │ ├── Expectation.ts │ ├── Invariant.ts │ ├── Event.ts │ ├── Repl.d.ts │ ├── Contract │ │ ├── Maximillion.ts │ │ ├── Counter.ts │ │ ├── InterestRateModel.ts │ │ ├── Vat.ts │ │ ├── PriceOracleProxy.ts │ │ ├── Reservoir.ts │ │ ├── Unitroller.ts │ │ ├── Pot.ts │ │ ├── ComptrollerImpl.ts │ │ ├── CErc20Delegate.ts │ │ ├── CErc20Delegator.ts │ │ ├── Erc20.ts │ │ ├── CompoundLens.ts │ │ ├── PriceOracle.ts │ │ ├── Timelock.ts │ │ ├── Comp.ts │ │ ├── Governor.ts │ │ └── builder.js │ ├── Action.ts │ ├── Assert.ts │ ├── Runner.ts │ ├── Invariant │ │ ├── SuccessInvariant.ts │ │ ├── StaticInvariant.ts │ │ └── RemainsInvariant.ts │ ├── Formatter.ts │ ├── Completer.ts │ ├── Value │ │ ├── UserValue.ts │ │ ├── MaximillionValue.ts │ │ ├── ComptrollerImplValue.ts │ │ ├── CTokenDelegateValue.ts │ │ ├── PriceOracleValue.ts │ │ ├── MCDValue.ts │ │ ├── InterestRateModelValue.ts │ │ └── PriceOracleProxyValue.ts │ ├── HistoricReadline.ts │ ├── Accounts.ts │ ├── Hypothetical.ts │ ├── Expectation │ │ ├── RemainsExpectation.ts │ │ └── ChangesExpectation.ts │ ├── File.ts │ ├── Encoding.ts │ ├── Builder │ │ ├── UnitrollerBuilder.ts │ │ └── MaximillionBuilder.ts │ ├── Event │ │ ├── TrxEvent.ts │ │ └── ExpectationEvent.ts │ ├── Web.ts │ ├── Help.ts │ ├── Settings.ts │ └── Macro.ts ├── script │ ├── generate_parser │ ├── webpack │ ├── tsc │ └── repl ├── webpack.config.js └── package.json ├── .soliumrc.json ├── contracts ├── PriceOracle.sol ├── CErc20Delegate.sol ├── InterestRateModel.sol ├── SimplePriceOracle.sol ├── CErc20Immutable.sol ├── Maximillion.sol └── CarefulMath.sol ├── truffle-config.js ├── Dockerfile ├── .dockerignore ├── brownie-config.yml ├── .gitignore ├── .solcover.js ├── package.json ├── LICENSE └── gasCosts.json /networks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/certora/CErc20Delegator: -------------------------------------------------------------------------------- 1 | CErc20 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/contracts/WBTC.sol 3 | -------------------------------------------------------------------------------- /tests/Jest.js: -------------------------------------------------------------------------------- 1 | 2 | /* global jest */ 3 | jest.setTimeout(300000); -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | Scripts to make common developer tasks easy to type. 2 | -------------------------------------------------------------------------------- /reporterConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter" 3 | } -------------------------------------------------------------------------------- /spec/certora/contracts/MathCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract MathCertora { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /scenario/src/Expectation.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Expectation { 3 | checker: (world: any) => Promise; 4 | } 5 | -------------------------------------------------------------------------------- /scenario/src/Invariant.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Invariant { 3 | held: boolean 4 | checker: (world: any) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /spec/certora/contracts/SimulationInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | interface SimulationInterface { 4 | function dummy() external; 5 | } 6 | -------------------------------------------------------------------------------- /networks/kovan-fork.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://kovan.infura.io/v3/e1a5d4d2c06a4e81945fca56d0d5d8ea", 3 | "unlocked": ["0xA776184Fd6F545DAe5f51361dBcC9018549a9749"] 4 | } 5 | -------------------------------------------------------------------------------- /scenario/src/Event.ts: -------------------------------------------------------------------------------- 1 | 2 | type ScalarEvent = string; 3 | interface EventArray extends Array { 4 | [index: number]: ScalarEvent | EventArray; 5 | } 6 | 7 | export type Event = EventArray; 8 | -------------------------------------------------------------------------------- /networks/mainnet-fork.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://mainnet.infura.io/v3/e1a5d4d2c06a4e81945fca56d0d5d8ea", 3 | "unlocked": ["0xfe83aF639f769EaD20baD76067AbC120245a06A9", "0x8b8592e9570e96166336603a1b4bd1e8db20fa20"] 4 | } 5 | -------------------------------------------------------------------------------- /spec/sim/0003-borrow-caps-patch/deploy.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn repl -n mainnet -s 2 | 3 | PrintTransactionLogs 4 | 5 | -- Deploy the flywheel impl 6 | ComptrollerImpl Deploy Standard StdComptrollerG5 7 | 8 | Print "Deployed OK!" 9 | -------------------------------------------------------------------------------- /scenario/src/Repl.d.ts: -------------------------------------------------------------------------------- 1 | import {Artifacts} from './Artifact'; 2 | import {Web3} from './Web3'; 3 | 4 | declare namespace NodeJS { 5 | interface Global { 6 | Web3: Web3 7 | Artifacts: Artifacts 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/sim/0001-comp-distribution-patch/deploy.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn repl -n mainnet -s 2 | 3 | PrintTransactionLogs 4 | 5 | -- Deploy the flywheel impl 6 | ComptrollerImpl Deploy Standard StdComptrollerG4 7 | 8 | Print "Deployed OK!" 9 | -------------------------------------------------------------------------------- /tests/Contracts/TetherInterface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/EIP20Interface.sol"; 4 | 5 | contract TetherInterface is EIP20Interface { 6 | function setParams(uint newBasisPoints, uint newMaxFee) external; 7 | } -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | DIR=`dirname $0` 6 | PROJ_ROOT="$DIR/.." 7 | 8 | "$PROJ_ROOT/node_modules/.bin/solium" -d "$PROJ_ROOT/tests" 9 | "$PROJ_ROOT/node_modules/.bin/solium" -d "$PROJ_ROOT/contracts" 10 | -------------------------------------------------------------------------------- /tests/Contracts/FalseMarker.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract FalseMarkerMethodComptroller { 4 | bool public constant isComptroller = false; 5 | } 6 | 7 | contract FalseMarkerMethodInterestRateModel { 8 | bool public constant isInterestRateModel = false; 9 | } 10 | -------------------------------------------------------------------------------- /spec/certora/contracts/CDaiDelegateCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/CDaiDelegate.sol"; 4 | 5 | contract CDaiDelegateCertora is CDaiDelegate { 6 | function getCashOf(address account) public view returns (uint) { 7 | return EIP20Interface(underlying).balanceOf(account); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Contracts/MathHelpers.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract MathHelpers { 4 | 5 | /* 6 | * @dev Creates a number like 15e16 as a uint256 from scientific(15, 16). 7 | */ 8 | function scientific(uint val, uint expTen) pure internal returns (uint) { 9 | return val * ( 10 ** expTen ); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /scenario/src/Contract/Maximillion.ts: -------------------------------------------------------------------------------- 1 | import {Contract} from '../Contract'; 2 | import {Callable, Sendable} from '../Invokation'; 3 | 4 | interface MaximillionMethods { 5 | cEther(): Callable 6 | repayBehalf(string): Sendable 7 | } 8 | 9 | export interface Maximillion extends Contract { 10 | methods: MaximillionMethods 11 | } 12 | -------------------------------------------------------------------------------- /scenario/src/Contract/Counter.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { encodedNumber } from '../Encoding'; 3 | import { Callable, Sendable } from '../Invokation'; 4 | 5 | export interface CounterMethods { 6 | increment(by: encodedNumber): Sendable; 7 | } 8 | 9 | export interface Counter extends Contract { 10 | methods: CounterMethods; 11 | name: string; 12 | } 13 | -------------------------------------------------------------------------------- /scenario/src/Action.ts: -------------------------------------------------------------------------------- 1 | import {Invokation} from './Invokation'; 2 | 3 | export class Action { 4 | log: string; 5 | invokation: Invokation; 6 | 7 | constructor(log: string, invokation: Invokation) { 8 | this.log = log; 9 | this.invokation = invokation; 10 | } 11 | 12 | toString() { 13 | return `Action: log=${this.log}, result=${this.invokation.toString()}`; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/certora/contracts/PriceOracleModel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/PriceOracle.sol"; 4 | 5 | contract PriceOracleModel is PriceOracle { 6 | uint dummy; 7 | 8 | function isPriceOracle() external pure returns (bool) { 9 | return true; 10 | } 11 | 12 | function getUnderlyingPrice(CToken cToken) external view returns (uint) { 13 | return dummy; 14 | } 15 | } -------------------------------------------------------------------------------- /scenario/script/generate_parser: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | scenario_dir="$(cd $dir/.. && pwd)" 7 | 8 | "$scenario_dir/node_modules/.bin/pegjs" \ 9 | --plugin "$scenario_dir/node_modules/ts-pegjs" \ 10 | -o "$scenario_dir/src/Parser.ts" \ 11 | --cache \ 12 | --allowed-start-rules tests,step,macros \ 13 | "$scenario_dir/Grammar.pegjs" 14 | -------------------------------------------------------------------------------- /spec/scenario/ChangeDelegate.scen: -------------------------------------------------------------------------------- 1 | -- Delegate upgrade tests 2 | 3 | Test "Change the delegate" 4 | NewComptroller 5 | NewCToken DEL cDEL 6 | Support cDEL collateralFactor:0.5 7 | Prep Jared Some DEL cDEL 8 | Mint Jared 100e18 cDEL 9 | CTokenDelegate Deploy CErc20Delegate cErc20Delegate2 10 | CToken cDEL SetImplementation (CTokenDelegate cErc20Delegate2 Address) True "0x0" 11 | Redeem Jared 50e9 cDEL 12 | -------------------------------------------------------------------------------- /tests/Contracts/GovernorAlphaHarness.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../../contracts/Governance/GovernorAlpha.sol"; 5 | 6 | contract GovernorAlphaHarness is GovernorAlpha { 7 | constructor(address timelock_, address comp_, address guardian_) GovernorAlpha(timelock_, comp_, guardian_) public {} 8 | 9 | function votingPeriod() public pure returns (uint) { return 240; } 10 | } 11 | -------------------------------------------------------------------------------- /scenario/src/Contract/InterestRateModel.ts: -------------------------------------------------------------------------------- 1 | import {Contract} from '../Contract'; 2 | import {Callable, Sendable} from '../Invokation'; 3 | import {encodedNumber} from '../Encoding'; 4 | 5 | interface InterestRateModelMethods { 6 | getBorrowRate(cash: encodedNumber, borrows: encodedNumber, reserves: encodedNumber): Callable 7 | } 8 | 9 | export interface InterestRateModel extends Contract { 10 | methods: InterestRateModelMethods 11 | name: string 12 | } 13 | -------------------------------------------------------------------------------- /scenario/src/Contract/Vat.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { encodedNumber } from '../Encoding'; 4 | 5 | interface VatMethods { 6 | dai(address: string): Callable; 7 | hope(address: string): Sendable; 8 | move(src: string, dst: string, amount: encodedNumber): Sendable; 9 | } 10 | 11 | export interface Vat extends Contract { 12 | methods: VatMethods; 13 | name: string; 14 | } 15 | -------------------------------------------------------------------------------- /scenario/src/Contract/PriceOracleProxy.ts: -------------------------------------------------------------------------------- 1 | import {Contract} from '../Contract'; 2 | import {Callable, Sendable} from '../Invokation'; 3 | import {encodedNumber} from '../Encoding'; 4 | 5 | interface PriceOracleProxyMethods { 6 | getUnderlyingPrice(asset: string): Callable 7 | v1PriceOracle(): Callable; 8 | setSaiPrice(amount: encodedNumber): Sendable 9 | } 10 | 11 | export interface PriceOracleProxy extends Contract { 12 | methods: PriceOracleProxyMethods 13 | } 14 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": "off", 12 | "max-len": [ 13 | "error", 14 | 200 15 | ], 16 | "security/no-block-members": "off", 17 | "security/no-inline-assembly": "off", 18 | "security/no-low-level-calls": "off", 19 | "security/no-tx-origin": "off", 20 | "imports-on-top": "off" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scenario/src/Assert.ts: -------------------------------------------------------------------------------- 1 | export type Expect = (actual: any) => { 2 | toEqual: (expected: any) => any 3 | fail: (message: string) => any 4 | } 5 | 6 | export const throwExpect: Expect = (x) => { 7 | return { 8 | toEqual: (y) => { 9 | let xEnc = JSON.stringify(x); 10 | let yEnc = JSON.stringify(y); 11 | if (xEnc !== yEnc) { 12 | throw new Error(`expected ${x} to equal ${y}`); 13 | } 14 | }, 15 | fail: (reason) => { 16 | throw new Error(reason) 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /tests/Contracts/ComptrollerScenarioG2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/ComptrollerG2.sol"; 4 | 5 | contract ComptrollerScenarioG2 is ComptrollerG2 { 6 | uint public blockNumber; 7 | 8 | constructor() ComptrollerG2() public {} 9 | 10 | function fastForward(uint blocks) public returns (uint) { 11 | blockNumber += blocks; 12 | return blockNumber; 13 | } 14 | 15 | function setBlockNumber(uint number) public { 16 | blockNumber = number; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scenario/src/Contract/Reservoir.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { encodedNumber } from '../Encoding'; 3 | import { Callable, Sendable } from '../Invokation'; 4 | 5 | export interface ReservoirMethods { 6 | drip(): Sendable; 7 | dripped(): Callable; 8 | dripStart(): Callable; 9 | dripRate(): Callable; 10 | token(): Callable; 11 | target(): Callable; 12 | } 13 | 14 | export interface Reservoir extends Contract { 15 | methods: ReservoirMethods; 16 | name: string; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Utils/JS.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function dfn(val, def) { 4 | return isFinite(val) ? val : def; 5 | } 6 | 7 | function last(elems) { 8 | return Array.isArray(elems) ? elems[elems.length - 1] : elems; 9 | } 10 | 11 | function lookup(obj, path = []) { 12 | return Array.isArray(path) ? path.reduce((a, k) => a[k], obj) : obj[path]; 13 | } 14 | 15 | function select(obj, keys = []) { 16 | return keys.reduce((a, k) => (a[k] = obj[k], a), {}) 17 | } 18 | 19 | module.exports = { 20 | dfn, 21 | last, 22 | lookup, 23 | select 24 | }; 25 | -------------------------------------------------------------------------------- /scenario/script/webpack: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | scenario_dir="$(cd $dir/.. && pwd)" 7 | 8 | cd "$scenario_dir" && 9 | mkdir -p ./build && 10 | ./node_modules/.bin/webpack \ 11 | --mode production \ 12 | --config ./webpack.config.js \ 13 | --entry ./src/Web.ts \ 14 | --module-bind 'ts=ts-loader' \ 15 | --module-bind 'exports-loader?parse' \ 16 | --resolve-extensions ".ts,.js" \ 17 | --output-library-target window \ 18 | --output ./build/scenario.js 19 | -------------------------------------------------------------------------------- /tests/Contracts/FixedPriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/PriceOracle.sol"; 4 | 5 | contract FixedPriceOracle is PriceOracle { 6 | uint public price; 7 | 8 | constructor(uint _price) public { 9 | price = _price; 10 | } 11 | 12 | function getUnderlyingPrice(CToken cToken) public view returns (uint) { 13 | cToken; 14 | return price; 15 | } 16 | 17 | function assetPrices(address asset) public view returns (uint) { 18 | asset; 19 | return price; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/scenario/ReEntry.scen: -------------------------------------------------------------------------------- 1 | 2 | Test "ReEntry Mint @no-cov" 3 | NewComptroller 4 | Erc20 Deploy ReEntrant PHREAK PHREAK "transferFrom" "mint(uint256)" "0" 5 | InterestRateModel Deploy Fixed Std 0.000001 6 | CToken Deploy Scenario cPHREAK cPHREAK (Erc20 PHREAK Address) (Comptroller Address) (InterestRateModel Std Address) 1e9 8 Admin 7 | Comptroller SupportMarket cPHREAK 8 | Prep Geoff Some PHREAK cPHREAK 9 | AllowFailures 10 | Mint Geoff 50e18 cPHREAK 11 | Assert Revert "revert re-entered" 12 | Assert Equal (Erc20 cPHREAK TokenBalance Geoff) Zero 13 | -------------------------------------------------------------------------------- /spec/certora/cDAI/mcd.cvl: -------------------------------------------------------------------------------- 1 | 2 | rule alwaysHasDai(uint256 amount) { 3 | env e0; 4 | 5 | require amount == sinvoke getCashOf(e0, e0.msg.sender); 6 | uint256 mintError = sinvoke mint(e0, amount); 7 | uint256 redeemError = invoke redeemUnderlying(e0, amount); 8 | bool redeemReverted = lastReverted; 9 | uint256 redeemedAmount = sinvoke getCashOf(e0, e0.msg.sender); 10 | 11 | // XXX what if it reverts? cannot fix until specs can run again 12 | assert !redeemReverted, "Redeem reverted"; 13 | assert !redeemReverted => redeemedAmount == amount, "Redeemed amount other than specified"; 14 | } -------------------------------------------------------------------------------- /contracts/PriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./CToken.sol"; 4 | 5 | contract PriceOracle { 6 | /// @notice Indicator that this is a PriceOracle contract (for inspection) 7 | bool public constant isPriceOracle = true; 8 | 9 | /** 10 | * @notice Get the underlying price of a cToken asset 11 | * @param cToken The cToken to get the underlying price of 12 | * @return The underlying asset price mantissa (scaled by 1e18). 13 | * Zero means the price is unavailable. 14 | */ 15 | function getUnderlyingPrice(CToken cToken) external view returns (uint); 16 | } 17 | -------------------------------------------------------------------------------- /spec/certora/contracts/CEtherCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/CEther.sol"; 4 | 5 | contract CEtherCertora is CEther { 6 | constructor(ComptrollerInterface comptroller_, 7 | InterestRateModel interestRateModel_, 8 | uint initialExchangeRateMantissa_, 9 | string memory name_, 10 | string memory symbol_, 11 | uint8 decimals_, 12 | address payable admin_) public CEther(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_, admin_) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scenario/src/Contract/Unitroller.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | 4 | interface UnitrollerMethods { 5 | admin(): Callable; 6 | pendingAdmin(): Callable; 7 | _acceptAdmin(): Sendable; 8 | _setPendingAdmin(pendingAdmin: string): Sendable; 9 | _setPendingImplementation(pendingImpl: string): Sendable; 10 | comptrollerImplementation(): Callable; 11 | pendingComptrollerImplementation(): Callable; 12 | } 13 | 14 | export interface Unitroller extends Contract { 15 | methods: UnitrollerMethods; 16 | } 17 | -------------------------------------------------------------------------------- /spec/certora/contracts/MaximillionCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/Maximillion.sol"; 4 | 5 | contract MaximillionCertora is Maximillion { 6 | constructor(CEther cEther_) public Maximillion(cEther_) {} 7 | 8 | function borrowBalance(address account) external returns (uint) { 9 | return cEther.borrowBalanceCurrent(account); 10 | } 11 | 12 | function etherBalance(address account) external returns (uint) { 13 | return account.balance; 14 | } 15 | 16 | function repayBehalf(address borrower) public payable { 17 | return super.repayBehalf(borrower); 18 | } 19 | } -------------------------------------------------------------------------------- /scenario/src/Contract/Pot.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { encodedNumber } from '../Encoding'; 4 | 5 | interface PotMethods { 6 | chi(): Callable; 7 | dsr(): Callable; 8 | rho(): Callable; 9 | pie(address: string): Callable; 10 | drip(): Sendable; 11 | file(what: string, data: encodedNumber): Sendable; 12 | join(amount: encodedNumber): Sendable; 13 | exit(amount: encodedNumber): Sendable; 14 | } 15 | 16 | export interface Pot extends Contract { 17 | methods: PotMethods; 18 | name: string; 19 | } 20 | -------------------------------------------------------------------------------- /scenario/src/Runner.ts: -------------------------------------------------------------------------------- 1 | import {World} from './World'; 2 | import {parse} from './Parser'; 3 | import {expandEvent, Macros} from './Macro'; 4 | import {processEvents} from './CoreEvent' 5 | 6 | export async function runCommand(world: World, command: string, macros: Macros): Promise { 7 | const trimmedCommand = command.trim(); 8 | 9 | const event = parse(trimmedCommand, {startRule: 'step'}); 10 | 11 | if (event === null) { 12 | return world; 13 | } else { 14 | world.printer.printLine(`Command: ${trimmedCommand}`); 15 | 16 | let expanded = expandEvent(macros, event); 17 | 18 | return processEvents(world, expanded); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scenario/src/Invariant/SuccessInvariant.ts: -------------------------------------------------------------------------------- 1 | import {Invariant} from '../Invariant'; 2 | import {fail, World} from '../World'; 3 | import {getCoreValue} from '../CoreValue'; 4 | import {Value} from '../Value'; 5 | import {Event} from '../Event'; 6 | 7 | export class SuccessInvariant implements Invariant { 8 | held = false; 9 | 10 | constructor() {} 11 | 12 | async checker(world: World): Promise { 13 | if (world.lastInvokation && !world.lastInvokation.success()) { 14 | fail(world, `Success invariant broken! Expected successful execution, but had error ${world.lastInvokation.toString()}`); 15 | } 16 | } 17 | 18 | toString() { 19 | return `SuccessInvariant`; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scenario/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | stats: 'verbose', 6 | devtool: 'source-map', 7 | externals: { 8 | file: '{}', 9 | fs: '{}', 10 | tls: '{}', 11 | net: '{}', 12 | xmlhttprequest: '{}', 13 | 'truffle-flattener': '{}', 14 | 'request': '{}' 15 | }, 16 | optimization: { 17 | minimize: true 18 | }, 19 | plugins: [ 20 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | // This has effect on the react lib size 24 | 'NODE_ENV': JSON.stringify('production'), 25 | } 26 | }) 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Uncommenting the defaults below 3 | // provides for an easier quick-start with Ganache. 4 | // You can also follow this format for other networks; 5 | // see 6 | // for more details on how to specify configuration options! 7 | // 8 | //networks: { 9 | // development: { 10 | // host: "127.0.0.1", 11 | // port: 7545, 12 | // network_id: "*" 13 | // }, 14 | // test: { 15 | // host: "127.0.0.1", 16 | // port: 7545, 17 | // network_id: "*" 18 | // } 19 | //} 20 | // 21 | compilers: { 22 | solc: { 23 | version: "0.6.12" 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /scenario/src/Contract/ComptrollerImpl.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { encodedNumber } from '../Encoding'; 4 | 5 | interface ComptrollerImplMethods { 6 | _become( 7 | comptroller: string, 8 | priceOracle?: string, 9 | maxAssets?: encodedNumber, 10 | closeFactor?: encodedNumber, 11 | reinitializing?: boolean 12 | ): Sendable; 13 | 14 | _become( 15 | comptroller: string, 16 | compRate: encodedNumber, 17 | compMarkets: string[], 18 | otherMarkets: string[] 19 | ): Sendable; 20 | } 21 | 22 | export interface ComptrollerImpl extends Contract { 23 | methods: ComptrollerImplMethods; 24 | } 25 | -------------------------------------------------------------------------------- /tests/Contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract Counter { 4 | uint public count; 5 | uint public count2; 6 | 7 | function increment(uint amount) public payable { 8 | count += amount; 9 | } 10 | 11 | function decrement(uint amount) public payable { 12 | require(amount <= count, "counter underflow"); 13 | count -= amount; 14 | } 15 | 16 | function increment(uint amount, uint amount2) public payable { 17 | count += amount; 18 | count2 += amount2; 19 | } 20 | 21 | function notZero() public view { 22 | require(count != 0, "Counter::notZero"); 23 | } 24 | 25 | function doRevert() public pure { 26 | require(false, "Counter::revert Testing"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/certora/contracts/InterestRateModelModel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/Exponential.sol"; 4 | import "../../../contracts/InterestRateModel.sol"; 5 | 6 | contract InterestRateModelModel is InterestRateModel { 7 | uint borrowDummy; 8 | uint supplyDummy; 9 | 10 | function isInterestRateModel() external pure returns (bool) { 11 | return true; 12 | } 13 | 14 | function getBorrowRate(uint _cash, uint _borrows, uint _reserves) external view returns (uint) { 15 | return borrowDummy; 16 | } 17 | 18 | function getSupplyRate(uint _cash, uint _borrows, uint _reserves, uint _reserveFactorMantissa) external view returns (uint) { 19 | return supplyDummy; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scenario/src/Formatter.ts: -------------------------------------------------------------------------------- 1 | import {Event} from './Event'; 2 | 3 | // Effectively the opposite of parse 4 | export function formatEvent(event: Event, outter=true): string { 5 | if (Array.isArray(event)) { 6 | if (event.length === 2 && typeof event[0] === "string" && (event[0]).toLowerCase() === "exactly") { 7 | return event[1].toString(); 8 | } 9 | 10 | let mapped = event.map(e => formatEvent(e, false)); 11 | let joined = mapped.join(' '); 12 | 13 | if (outter) { 14 | return joined; 15 | } else { 16 | return `(${joined})`; 17 | } 18 | } else { 19 | return event; 20 | } 21 | } 22 | 23 | export function formatError(err: any) { 24 | return JSON.stringify(err); // yeah... for now 25 | } 26 | -------------------------------------------------------------------------------- /script/verify: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | set -x 5 | 6 | cd `dirname $0`/.. 7 | 8 | command -v java >/dev/null 2>&1 || { echo "Error: java is not installed." >&2; exit 1; } 9 | command -v z3 >/dev/null 2>&1 || { echo "Error: z3 is not installed." >&2; exit 1; } 10 | 11 | function verify_spec { 12 | [ -e "$1" ] || (echo "spec file not found: $1" && exit 1) 13 | make -B "$1" 14 | } 15 | 16 | if [ "$CI" ]; then 17 | all_specs=($(echo "" | circleci tests split --split-by=timings)) 18 | elif [[ $# -eq 0 ]] ; then 19 | all_specs=(spec/certora/{Comp,Governor}/*.cvl) 20 | else 21 | all_specs=($@) 22 | fi 23 | 24 | echo "running specs ${all_specs[@]}" 25 | 26 | for spec in "${all_specs[@]}"; do 27 | verify_spec "$spec" 28 | done 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:13.8.0 2 | 3 | RUN apk update && apk add --no-cache --virtual build-dependencies git python g++ make 4 | RUN wget https://github.com/ethereum/solidity/releases/download/v0.5.16/solc-static-linux -O /bin/solc && chmod +x /bin/solc 5 | 6 | RUN mkdir -p /compound-protocol 7 | WORKDIR /compound-protocol 8 | 9 | # First add deps 10 | ADD ./package.json /compound-protocol 11 | ADD ./yarn.lock /compound-protocol 12 | RUN yarn install --lock-file 13 | 14 | # Then rest of code and build 15 | ADD . /compound-protocol 16 | 17 | ENV SADDLE_SHELL=/bin/sh 18 | ENV SADDLE_CONTRACTS="contracts/*.sol contracts/**/*.sol" 19 | RUN npx saddle compile 20 | 21 | RUN apk del build-dependencies 22 | RUN yarn cache clean 23 | 24 | CMD while :; do sleep 2073600; done 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | allFiredEvents 2 | build 3 | build_ 4 | node_modules 5 | scenario/node_modules 6 | .env 7 | .certora* 8 | certora_* 9 | coverage/ 10 | coverage.json 11 | emv-*/ 12 | networks/test.json 13 | networks/test-abi.json 14 | networks/coverage.json 15 | networks/coverage-abi.json 16 | networks/development.json 17 | networks/development-abi.json 18 | networks/coverage-contracts/* 19 | networks/test-contracts/* 20 | networks/*-contracts.json 21 | networks/*-history 22 | networks/*-settings.json 23 | *.DS_Store 24 | test-results.xml 25 | .tsbuilt 26 | yarn-error.log 27 | scenario/build/webpack.js 28 | .scencache 29 | .solcache 30 | .solcachecov 31 | scenario/.tscache 32 | tests 33 | spec 34 | junit.xml 35 | .build 36 | .last_confs 37 | .git 38 | script/certora 39 | .saddle_history 40 | .circleci 41 | -------------------------------------------------------------------------------- /tests/Contracts/ComptrollerScenarioG4.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/ComptrollerG4.sol"; 4 | 5 | contract ComptrollerScenarioG4 is ComptrollerG4 { 6 | uint public blockNumber; 7 | 8 | constructor() ComptrollerG4() public {} 9 | 10 | function fastForward(uint blocks) public returns (uint) { 11 | blockNumber += blocks; 12 | return blockNumber; 13 | } 14 | 15 | function setBlockNumber(uint number) public { 16 | blockNumber = number; 17 | } 18 | 19 | function membershipLength(CToken cToken) public view returns (uint) { 20 | return accountAssets[address(cToken)].length; 21 | } 22 | 23 | function unlist(CToken cToken) public { 24 | markets[address(cToken)].isListed = false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /brownie-config.yml: -------------------------------------------------------------------------------- 1 | # use Ganache's forked mainnet mode as the default network 2 | # NOTE: You don't *have* to do this, but it is often helpful for testing 3 | networks: 4 | default: fantom-archive-fork 5 | # automatically fetch contract sources from Etherscan 6 | autofetch_sources: True 7 | 8 | # require OpenZepplin Contracts 9 | dependencies: 10 | - iearn-finance/yearn-vaults@0.4.3 11 | - OpenZeppelin/openzeppelin-contracts@3.1.0 12 | 13 | # path remapping to support imports from GitHub/NPM 14 | compiler: 15 | solc: 16 | version: 17 | optimize: true 18 | runs: 200 19 | remappings: 20 | - "@yearnvaults=iearn-finance/yearn-vaults@0.4.3" 21 | - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.1.0" 22 | 23 | reports: 24 | exclude_contracts: 25 | - SafeMath 26 | -------------------------------------------------------------------------------- /tests/Contracts/Const.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | contract ConstBase { 4 | uint public constant C = 1; 5 | 6 | function c() public pure returns (uint) { 7 | return 1; 8 | } 9 | 10 | function ADD(uint a) public view returns (uint) { 11 | // tells compiler to accept view instead of pure 12 | if (false) { 13 | C + now; 14 | } 15 | return a + C; 16 | } 17 | 18 | function add(uint a) public view returns (uint) { 19 | // tells compiler to accept view instead of pure 20 | if (false) { 21 | C + now; 22 | } 23 | return a + c(); 24 | } 25 | } 26 | 27 | contract ConstSub is ConstBase { 28 | function c() public pure returns (uint) { 29 | return 2; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Contracts/Structs.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | pragma experimental ABIEncoderV2; 3 | 4 | contract Structs { 5 | struct Outer { 6 | uint sentinel; 7 | mapping(address => Inner) inners; 8 | } 9 | 10 | struct Inner { 11 | uint16 a; 12 | uint16 b; 13 | uint96 c; 14 | } 15 | 16 | mapping(uint => Outer) public outers; 17 | 18 | function writeEach(uint id, uint16 a, uint16 b, uint96 c) public { 19 | Inner storage inner = outers[id].inners[msg.sender]; 20 | inner.a = a; 21 | inner.b = b; 22 | inner.c = c; 23 | } 24 | 25 | function writeOnce(uint id, uint16 a, uint16 b, uint96 c) public { 26 | Inner memory inner = Inner({a: a, b: b, c: c}); 27 | outers[id].inners[msg.sender] = inner; 28 | } 29 | } -------------------------------------------------------------------------------- /scenario/src/Contract/CErc20Delegate.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Sendable } from '../Invokation'; 3 | import { CTokenMethods, CTokenScenarioMethods } from './CToken'; 4 | 5 | interface CErc20DelegateMethods extends CTokenMethods { 6 | _becomeImplementation(data: string): Sendable; 7 | _resignImplementation(): Sendable; 8 | } 9 | 10 | interface CErc20DelegateScenarioMethods extends CTokenScenarioMethods { 11 | _becomeImplementation(data: string): Sendable; 12 | _resignImplementation(): Sendable; 13 | } 14 | 15 | export interface CErc20Delegate extends Contract { 16 | methods: CErc20DelegateMethods; 17 | name: string; 18 | } 19 | 20 | export interface CErc20DelegateScenario extends Contract { 21 | methods: CErc20DelegateScenarioMethods; 22 | name: string; 23 | } 24 | -------------------------------------------------------------------------------- /tests/Contracts/MockMCD.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | 4 | contract MockPot { 5 | 6 | uint public dsr; // the Dai Savings Rate 7 | 8 | constructor(uint dsr_) public { 9 | setDsr(dsr_); 10 | } 11 | 12 | function setDsr(uint dsr_) public { 13 | dsr = dsr_; 14 | } 15 | } 16 | 17 | contract MockJug { 18 | 19 | struct Ilk { 20 | uint duty; 21 | uint rho; 22 | } 23 | 24 | mapping (bytes32 => Ilk) public ilks; 25 | uint public base; 26 | 27 | constructor(uint duty_, uint base_) public { 28 | setETHDuty(duty_); 29 | setBase(base_); 30 | } 31 | 32 | function setBase(uint base_) public { 33 | base = base_; 34 | } 35 | 36 | function setETHDuty(uint duty_) public { 37 | ilks["ETH-A"].duty = duty_; 38 | } 39 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | allFiredEvents 2 | .build-temp 3 | build 4 | build_ 5 | node_modules 6 | .env 7 | .certora* 8 | certora_* 9 | coverage/ 10 | coverage.json 11 | coverageEnv/ 12 | emv-*/ 13 | formulas/ 14 | networks/test.json 15 | networks/test-abi.json 16 | networks/coverage.json 17 | networks/coverage-abi.json 18 | networks/development.json 19 | networks/development-abi.json 20 | networks/coverage-contracts/* 21 | networks/test-contracts/* 22 | networks/*-contracts.json 23 | networks/*-history 24 | networks/*-settings.json 25 | outputs/ 26 | Reports/ 27 | scTopics 28 | *.DS_Store 29 | test-results.xml 30 | .tsbuilt 31 | yarn-error.log 32 | scenario/build/webpack.js 33 | .scencache 34 | .solcache 35 | .solcachecov 36 | scenario/.tscache 37 | script/certora 38 | tests/scenarios/ 39 | junit.xml 40 | .build 41 | .last_confs 42 | .saddle_history 43 | -------------------------------------------------------------------------------- /script/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | proj_root="$dir/.." 7 | test_root="$dir/../tests" 8 | contracts_root="$dir/../contracts" 9 | coverage_root="$dir/../coverage" 10 | network=${NETWORK:-test} 11 | verbose=${verbose:-} 12 | 13 | # Compile scenario runner 14 | [[ -z $NO_TSC ]] && "$proj_root/scenario/script/tsc" 15 | 16 | # Build scenario stubs 17 | [[ -z $NO_BUILD_STUB ]] && "$proj_root/script/compile" --trace 18 | 19 | # Build scenario stubs 20 | [[ -z $NO_BUILD_SCEN ]] && "$proj_root/script/build_scenarios" 21 | 22 | rm -rf "$coverage_root" 23 | 24 | npx saddle coverage $@ 25 | coverage_code=$? 26 | 27 | npx istanbul report --root="$coverage_root" lcov json 28 | 29 | echo "Coverage generated. Report at $(cd $coverage_root && pwd)/lcov-report/index.html" 30 | 31 | exit $coverage_code 32 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const {execSync} = require('child_process'); 2 | async function moveCoverage(config) { 3 | execSync('mv ./.coverage_artifacts/contracts ./networks/coverage-contracts'); 4 | } 5 | 6 | async function moveCoverageBack() { 7 | execSync('mv ./networks/coverage-contracts ./.coverage_artifacts/contracts'); 8 | } 9 | 10 | module.exports = { 11 | port: 8555, 12 | providerOpts: 13 | { // See example coverage settings at https://github.com/sc-forks/solidity-coverage 14 | gas: 0xfffffff, 15 | gasPrice: 0x01 16 | }, 17 | mocha: { 18 | enableTimeouts: false, 19 | grep: /@gas|@no-cov/, 20 | invert: true 21 | }, 22 | onCompileComplete: moveCoverage, 23 | onTestsComplete: moveCoverageBack, 24 | skipFiles: ['test'].concat( 25 | process.env['SKIP_UNITROLLER'] ? ['Unitroller.sol'] : []), 26 | }; 27 | -------------------------------------------------------------------------------- /spec/certora/Math/int.cvl: -------------------------------------------------------------------------------- 1 | 2 | basicDiv(uint a, uint b) { 3 | require b > 0; 4 | 5 | uint c = a + 1; 6 | uint d = a / b; 7 | uint e = c / b; 8 | 9 | assert c >= a, "Failed to prove ${c} >= ${a}"; 10 | assert e >= d, "Failed to prove: ${e} >= ${d}"; 11 | } 12 | 13 | atLeastEnough(uint256 chi, uint256 amount) { 14 | uint256 WAD = 1000000000000000000; 15 | uint256 RAY = 1000000000000000000000000000; 16 | uint256 MAX = 115792089237316195423570985008687907853269984665640564039457584007913129639935; 17 | 18 | require chi >= RAY; 19 | require amount <= MAX; 20 | 21 | assert (amount + 1) >= amount, "Failed (${amount} + 1) >= ${amount}"; 22 | assert (amount + 1) / chi >= amount / chi, "Failed: (${amount} + 1) / ${chi} >= ${amount} / ${chi}"; 23 | assert ((((amount + 1) * RAY) / chi) * chi / RAY) >= amount, "Not enough DAI"; 24 | } -------------------------------------------------------------------------------- /spec/certora/contracts/ComptrollerCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/Comptroller.sol"; 4 | 5 | contract ComptrollerCertora is Comptroller { 6 | uint8 switcher; 7 | uint liquidityOrShortfall; 8 | 9 | function getHypotheticalAccountLiquidityInternal( 10 | address account, 11 | CToken cTokenModify, 12 | uint redeemTokens, 13 | uint borrowAmount) internal view returns (Error, uint, uint) { 14 | if (switcher == 0) 15 | return (Error.NO_ERROR, liquidityOrShortfall, 0); 16 | if (switcher == 1) 17 | return (Error.NO_ERROR, 0, liquidityOrShortfall); 18 | if (switcher == 2) 19 | return (Error.SNAPSHOT_ERROR, 0, 0); 20 | if (switcher == 3) 21 | return (Error.PRICE_ERROR, 0, 0); 22 | return (Error.MATH_ERROR, 0, 0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | root_dir="$(cd $dir/.. && pwd)" 7 | cache_file="$root_dir/.solcache" 8 | saddle_config_file="$root_dir/saddle.config.js" 9 | config_trace=`node -e "console.log(require(\"$saddle_config_file\").trace)"` 10 | [[ "$*" == "--trace" ]] || [[ "$config_trace" == "true" ]] && cache_file="${cache_file}cov" 11 | checksum="$(ls $root_dir/{contracts,contracts/**,tests/Contracts}/*.sol | xargs shasum | shasum | cut -d' ' -f 1)" 12 | 13 | if [ -z "$rebuild" -a -f "$cache_file" ]; then 14 | cached="$(cat $cache_file)" 15 | 16 | if [ "$cached" == "$checksum" ]; then 17 | echo "Skipping Contract Rebuild (set rebuild=true to force)" 18 | exit 0 19 | fi 20 | fi 21 | 22 | echo "Compiling Solidity contracts..." 23 | npx saddle compile $@ 24 | 25 | echo "$checksum" > "$cache_file" 26 | -------------------------------------------------------------------------------- /spec/certora/contracts/CTokenCollateral.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/CErc20Immutable.sol"; 4 | import "../../../contracts/EIP20Interface.sol"; 5 | 6 | contract CTokenCollateral is CErc20Immutable { 7 | constructor(address underlying_, 8 | ComptrollerInterface comptroller_, 9 | InterestRateModel interestRateModel_, 10 | uint initialExchangeRateMantissa_, 11 | string memory name_, 12 | string memory symbol_, 13 | uint8 decimals_, 14 | address payable admin_) public CErc20Immutable(underlying_, comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_, admin_) { 15 | } 16 | 17 | function getCashOf(address account) public view returns (uint) { 18 | return EIP20Interface(underlying).balanceOf(account); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Tokens/registerAndUnregisterCollateral.js: -------------------------------------------------------------------------------- 1 | const { 2 | makeCToken 3 | } = require('../Utils/Compound'); 4 | 5 | const exchangeRate = 50e3; 6 | 7 | describe('CToken', function () { 8 | let root, admin, accounts; 9 | let cToken; 10 | 11 | beforeEach(async () => { 12 | [root, admin, ...accounts] = saddle.accounts; 13 | cToken = await makeCToken({kind: 'ccollateralcap', comptrollerOpts: {kind: 'bool'}, exchangeRate}); 14 | }); 15 | 16 | it("fails to register collateral for non comptroller", async () => { 17 | await expect(send(cToken, 'registerCollateral', [root])).rejects.toRevert("revert only comptroller may register collateral for user"); 18 | }); 19 | 20 | it("fails to unregister collateral for non comptroller", async () => { 21 | await expect(send(cToken, 'unregisterCollateral', [root])).rejects.toRevert("revert only comptroller may unregister collateral for user"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /scenario/src/Completer.ts: -------------------------------------------------------------------------------- 1 | import {World} from './World'; 2 | import {Macros} from './Macro'; 3 | 4 | // TODO: Get smarter about storing actions as data 5 | const actions: string[] = [ 6 | "Read", 7 | "Assert", 8 | "FastForward", 9 | "Inspect", 10 | "Debug", 11 | "From", 12 | "Invariant", 13 | "Comptroller", 14 | "cToken", 15 | "Erc20", 16 | ]; 17 | 18 | function caseInsensitiveSort(a: string, b: string): number { 19 | let A = a.toUpperCase(); 20 | let B = b.toUpperCase(); 21 | 22 | if (A < B) { 23 | return -1; 24 | } else if (A > B) { 25 | return 1; 26 | } else { 27 | return 0; 28 | } 29 | } 30 | 31 | export function complete(world: World, macros: Macros, line: string) { 32 | let allActions = actions.concat(Object.keys(macros)).sort(caseInsensitiveSort); 33 | const hits = allActions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase())); 34 | 35 | return [hits, line]; 36 | } 37 | -------------------------------------------------------------------------------- /networks/README.md: -------------------------------------------------------------------------------- 1 | ## Network Configuration 2 | 3 | This folder contains the configuration for given networks (e.g. `rinkeby.json` is the configuration for the Rinkeby test-net). These configuration files are meant to be used to configure external applications (like dApps) and thus contain a base set of information that may be useful (such as the address the Comptroller and a list of cToken markets). These configuration files are auto-generated when doing local development. 4 | 5 | Structure 6 | --------- 7 | 8 | ```json 9 | { 10 | "Contracts": { 11 | "MoneyMarket": "0x{address}", 12 | "Migrations": "0x{address}", 13 | "PriceOracle": "0x{address}", 14 | "InterestRateModel": "0x{address}" 15 | }, 16 | "Tokens": { 17 | "{SYM}": { 18 | "name": "{Full Name}", 19 | "symbol": "{SYM}", 20 | "decimals": 18, 21 | "address": "0x{address}", 22 | "supported": true 23 | } 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | proj_root="$dir/.." 7 | network=${NETWORK:-test} 8 | verbose=${verbose:-} 9 | 10 | debug_args="-n --inspect" #add "debugger" statements to javascript and interact with running code in repl found at chrome://inspect 11 | [[ -z $DEBUG ]] && debug_args="" 12 | 13 | # Compile scenario runner 14 | [[ -z $NO_TSC ]] && "$proj_root/scenario/script/tsc" 15 | 16 | # Build scenario stubs 17 | [[ -z $NO_BUILD_STUB ]] && "$proj_root/script/compile" 18 | 19 | # Build scenario stubs 20 | [[ -z $NO_BUILD_SCEN ]] && "$proj_root/script/build_scenarios" 21 | 22 | [[ -n $NO_RUN ]] && exit 0 23 | 24 | args=() 25 | for arg in "$@"; do 26 | mapped=`node -e "console.log(\"$arg\".replace(/spec\/scenario\/(.*).scen/i, \"tests/Scenarios/\\$1ScenTest.js\"))"` 27 | args+=("$mapped") 28 | done 29 | 30 | proj_root="$proj_root" verbose="$verbose" npx $debug_args saddle test "${args[@]}" 31 | -------------------------------------------------------------------------------- /scenario/src/Contract/CErc20Delegator.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { CTokenMethods } from './CToken'; 4 | import { encodedNumber } from '../Encoding'; 5 | 6 | interface CErc20DelegatorMethods extends CTokenMethods { 7 | implementation(): Callable; 8 | _setImplementation( 9 | implementation_: string, 10 | allowResign: boolean, 11 | becomImplementationData: string 12 | ): Sendable; 13 | } 14 | 15 | interface CErc20DelegatorScenarioMethods extends CErc20DelegatorMethods { 16 | setTotalBorrows(amount: encodedNumber): Sendable; 17 | setTotalReserves(amount: encodedNumber): Sendable; 18 | } 19 | 20 | export interface CErc20Delegator extends Contract { 21 | methods: CErc20DelegatorMethods; 22 | name: string; 23 | } 24 | 25 | export interface CErc20DelegatorScenario extends Contract { 26 | methods: CErc20DelegatorMethods; 27 | name: string; 28 | } 29 | -------------------------------------------------------------------------------- /spec/certora/Comp/search.cvl: -------------------------------------------------------------------------------- 1 | binarySearch(address account, uint blockNumber, uint futureBlock) { 2 | env e0; 3 | require e0.msg.value == 0; 4 | require blockNumber < e0.block.number; 5 | require futureBlock >= e0.block.number; 6 | 7 | uint nCheckpoints; 8 | require nCheckpoints <= 4; 9 | require invoke numCheckpoints(e0, account) == nCheckpoints; 10 | 11 | require invoke certoraOrdered(e0, account); 12 | 13 | invoke getPriorVotes(e0, account, futureBlock); 14 | assert lastReverted, "Must revert for future blocks"; 15 | 16 | uint votesLinear = invoke certoraScan(e0, account, blockNumber); 17 | assert !lastReverted, "Linear scan should not revert for any valid block number"; 18 | 19 | uint votesBinary = invoke getPriorVotes(e0, account, blockNumber); 20 | assert !lastReverted, "Query should not revert for any valid block number"; 21 | 22 | assert votesLinear == votesBinary, "Linear search and binary search disagree"; 23 | } 24 | -------------------------------------------------------------------------------- /spec/certora/contracts/TimelockCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/Timelock.sol"; 4 | 5 | contract TimelockCertora is Timelock { 6 | constructor(address admin_, uint256 delay_) public Timelock(admin_, delay_) {} 7 | 8 | function grace() pure public returns(uint256) { 9 | return GRACE_PERIOD; 10 | } 11 | 12 | function queueTransactionStatic(address target, uint256 value, uint256 eta) public returns (bytes32) { 13 | return queueTransaction(target, value, "setCounter()", "", eta); 14 | } 15 | 16 | function cancelTransactionStatic(address target, uint256 value, uint256 eta) public { 17 | return cancelTransaction(target, value, "setCounter()", "", eta); 18 | } 19 | 20 | function executeTransactionStatic(address target, uint256 value, uint256 eta) public { 21 | executeTransaction(target, value, "setCounter()", "", eta); // NB: cannot return dynamic types (will hang solver) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Contracts/TimelockHarness.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/Timelock.sol"; 4 | 5 | interface Administered { 6 | function _acceptAdmin() external returns (uint); 7 | } 8 | 9 | contract TimelockHarness is Timelock { 10 | constructor(address admin_, uint delay_) 11 | Timelock(admin_, delay_) public { 12 | } 13 | 14 | function harnessSetPendingAdmin(address pendingAdmin_) public { 15 | pendingAdmin = pendingAdmin_; 16 | } 17 | 18 | function harnessSetAdmin(address admin_) public { 19 | admin = admin_; 20 | } 21 | } 22 | 23 | contract TimelockTest is Timelock { 24 | constructor(address admin_, uint delay_) Timelock(admin_, 2 days) public { 25 | delay = delay_; 26 | } 27 | 28 | function harnessSetAdmin(address admin_) public { 29 | require(msg.sender == admin); 30 | admin = admin_; 31 | } 32 | 33 | function harnessAcceptAdmin(Administered administered) public { 34 | administered._acceptAdmin(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scenario/script/tsc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | scenario_dir="$(cd $dir/.. && pwd)" 7 | parent_dir="$(cd $dir/../.. && pwd)" 8 | cache_file="$scenario_dir/.tscache" 9 | checksum="$(echo $scenario_dir/src/{*,**/}*.* | xargs shasum | shasum | cut -d' ' -f 1)" 10 | 11 | if [ ! -d "$scenario_dir/node_modules" ]; then 12 | echo "Getting scenario packages..." 13 | cd "$scenario_dir" && yarn 14 | fi 15 | 16 | trap cleanup EXIT 17 | 18 | cleanup() { 19 | mv "$parent_dir/node_modules_tmp" "$parent_dir/node_modules" 20 | } 21 | 22 | mv "$parent_dir/node_modules" "$parent_dir/node_modules_tmp" 23 | 24 | if [ -z "$rebuild" -a -f "$cache_file" ]; then 25 | cached="$(cat $cache_file)" 26 | 27 | if [ "$cached" == "$checksum" ]; then 28 | echo "Skipping Scenario Rebuild (set rebuild=true to force)" 29 | exit 0 30 | fi 31 | fi 32 | 33 | echo "Building Scenario Runner..." 34 | cd "$scenario_dir" && node "$scenario_dir/node_modules/.bin/tsc" ${TSC_ARGS-"--skipLibCheck"} 35 | 36 | echo "$checksum" > "$cache_file" 37 | -------------------------------------------------------------------------------- /spec/certora/Maximillion/maximillion.cvl: -------------------------------------------------------------------------------- 1 | repayBehalf(uint256 repayAmount, address borrower) 2 | description "Break repayBehalf" { 3 | env e0; 4 | env e1; 5 | env e2; 6 | 7 | require e1.block.number == e0.block.number; 8 | require e2.block.number == e1.block.number; 9 | 10 | require e0.msg.value == 0; 11 | uint256 borrowed = sinvoke borrowBalance(e0, borrower); 12 | 13 | require repayAmount == e1.msg.value; 14 | invoke repayBehalf(e1, borrower); 15 | bool repayReverted = lastReverted; 16 | 17 | require e2.msg.value == 0; 18 | uint256 borrows = sinvoke borrowBalance(e2, borrower); 19 | uint256 balance = sinvoke etherBalance(e2, e1.msg.sender); 20 | 21 | assert (!repayReverted => 22 | ((repayAmount >= borrowed) => (balance >= repayAmount - borrowed))), "Mismatch in refund"; 23 | assert (!repayReverted => 24 | ((repayAmount >= borrowed) => (borrows == 0)) && 25 | ((repayAmount < borrowed) => (borrows == borrowed - repayAmount))), "Mismatch in borrows repaid"; 26 | } -------------------------------------------------------------------------------- /scenario/src/Contract/Erc20.ts: -------------------------------------------------------------------------------- 1 | import {Contract} from '../Contract'; 2 | import {Callable, Sendable} from '../Invokation'; 3 | import {encodedNumber} from '../Encoding'; 4 | 5 | interface Erc20Methods { 6 | name(): Callable 7 | symbol(): Callable 8 | decimals(): Callable 9 | totalSupply(): Callable 10 | balanceOf(string): Callable 11 | allowance(owner: string, spender: string): Callable 12 | approve(address: string, amount: encodedNumber): Sendable 13 | allocateTo(address: string, amount: encodedNumber): Sendable 14 | transfer(address: string, amount: encodedNumber): Sendable 15 | transferFrom(owner: string, spender: string, amount: encodedNumber): Sendable 16 | setFail(fail: boolean): Sendable 17 | pause(): Sendable 18 | unpause(): Sendable 19 | setParams(newBasisPoints: encodedNumber, maxFee: encodedNumber): Sendable 20 | } 21 | 22 | export interface Erc20 extends Contract { 23 | methods: Erc20Methods 24 | name: string 25 | } 26 | -------------------------------------------------------------------------------- /scenario/src/Value/UserValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import { 4 | getAddressV 5 | } from '../CoreValue'; 6 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 7 | import { 8 | AddressV, 9 | Value 10 | } from '../Value'; 11 | 12 | async function getUserAddress(world: World, user: string): Promise { 13 | return new AddressV(user); 14 | } 15 | 16 | export function userFetchers() { 17 | return [ 18 | new Fetcher<{account: AddressV}, AddressV>(` 19 | #### Address 20 | 21 | * "User Address" - Returns address of user 22 | * E.g. "User Geoff Address" - Returns Geoff's address 23 | `, 24 | "Address", 25 | [ 26 | new Arg("account", getAddressV) 27 | ], 28 | async (world, {account}) => account, 29 | {namePos: 1} 30 | ) 31 | ]; 32 | } 33 | 34 | export async function getUserValue(world: World, event: Event): Promise { 35 | return await getFetcherValue("User", userFetchers(), world, event); 36 | } 37 | -------------------------------------------------------------------------------- /scenario/src/HistoricReadline.ts: -------------------------------------------------------------------------------- 1 | import * as readline from 'readline'; 2 | import * as fs from 'fs'; 3 | import {readFile} from './File'; 4 | 5 | let readlineAny = readline; 6 | 7 | export async function createInterface(options): Promise { 8 | let history: string[] = await readFile(null, options['path'], [], (x) => x.split("\n")); 9 | let cleanHistory = history.filter((x) => !!x).reverse(); 10 | 11 | readlineAny.kHistorySize = Math.max(readlineAny.kHistorySize, options['maxLength']); 12 | 13 | let rl = readline.createInterface(options); 14 | let rlAny = rl; 15 | 16 | let oldAddHistory = rlAny._addHistory; 17 | 18 | rlAny._addHistory = function() { 19 | let last = rlAny.history[0]; 20 | let line = oldAddHistory.call(rl); 21 | 22 | // TODO: Should this be sync? 23 | if (line.length > 0 && line != last) { 24 | fs.appendFileSync(options['path'], `${line}\n`); 25 | } 26 | 27 | // TODO: Truncate file? 28 | 29 | return line; 30 | } 31 | 32 | rlAny.history.push.apply(rlAny.history, cleanHistory); 33 | 34 | return rl; 35 | } 36 | -------------------------------------------------------------------------------- /scenario/src/Contract/CompoundLens.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { encodedNumber } from '../Encoding'; 3 | import { Callable, Sendable } from '../Invokation'; 4 | 5 | export interface CompoundLensMethods { 6 | cTokenBalances(cToken: string, account: string): Sendable<[string,number,number,number,number,number]>; 7 | cTokenBalancesAll(cTokens: string[], account: string): Sendable<[string,number,number,number,number,number][]>; 8 | cTokenMetadata(cToken: string): Sendable<[string,number,number,number,number,number,number,number,number,boolean,number,string,number,number]>; 9 | cTokenMetadataAll(cTokens: string[]): Sendable<[string,number,number,number,number,number,number,number,number,boolean,number,string,number,number][]>; 10 | cTokenUnderlyingPrice(cToken: string): Sendable<[string,number]>; 11 | cTokenUnderlyingPriceAll(cTokens: string[]): Sendable<[string,number][]>; 12 | getAccountLimits(comptroller: string, account: string): Sendable<[string[],number,number]>; 13 | } 14 | 15 | export interface CompoundLens extends Contract { 16 | methods: CompoundLensMethods; 17 | name: string; 18 | } 19 | -------------------------------------------------------------------------------- /scenario/src/Accounts.ts: -------------------------------------------------------------------------------- 1 | import {World} from './World'; 2 | import {Map} from 'immutable'; 3 | 4 | export const accountMap = { 5 | "default": 0, 6 | "root": 0, 7 | "admin": 0, 8 | "first": 0, 9 | 10 | "bank": 1, 11 | "second": 1, 12 | 13 | "geoff": 2, 14 | "third": 2, 15 | "guardian": 2, 16 | 17 | "torrey": 3, 18 | "fourth": 3, 19 | 20 | "robert": 4, 21 | "fifth": 4, 22 | 23 | "coburn": 5, 24 | "sixth": 5, 25 | 26 | "jared": 6, 27 | "seventh": 6 28 | }; 29 | 30 | export interface Account { 31 | name: string 32 | address: string 33 | } 34 | 35 | export type Accounts = Map 36 | 37 | export function accountAliases(index: number): string[] { 38 | return Object.entries(accountMap).filter(([k,v]) => v === index).map(([k,v]) => k); 39 | } 40 | 41 | export function loadAccounts(accounts: string[]): Accounts { 42 | return Object.entries(accountMap).reduce((acc, [name, index]) => { 43 | if (accounts[index]) { 44 | return acc.set(name, { name: name, address: accounts[index] }); 45 | } else { 46 | return acc; 47 | } 48 | }, >Map({})); 49 | } 50 | -------------------------------------------------------------------------------- /scenario/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compound-protocol-alpha", 3 | "version": "0.2.1", 4 | "description": "The Compound Money Market", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "./script/webpack" 8 | }, 9 | "repository": "git@github.com:compound-finance/money-market.git", 10 | "author": "Compound Finance", 11 | "license": "UNLICENSED", 12 | "devDependencies": { 13 | "request": "^2.88.0", 14 | "solparse": "^2.2.8", 15 | "ts-loader": "^5.3.3", 16 | "ts-pegjs": "^0.2.2", 17 | "typescript": "^3.3.3", 18 | "webpack": "^4.29.6", 19 | "webpack-bundle-analyzer": "^3.1.0", 20 | "webpack-cli": "^3.3.0" 21 | }, 22 | "dependencies": { 23 | "bignumber.js": "8.0.1", 24 | "eth-saddle": "^0.1.17", 25 | "ethers": "^4.0.0-beta.1", 26 | "immutable": "^4.0.0-rc.12", 27 | "truffle-flattener": "^1.3.0", 28 | "web3": "^1.2.4" 29 | }, 30 | "resolutions": { 31 | "scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", 32 | "**/ganache-core": "https://github.com/compound-finance/ganache-core.git#jflatow/unbreak-fork" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scenario/src/Hypothetical.ts: -------------------------------------------------------------------------------- 1 | import {Accounts, loadAccounts} from './Accounts'; 2 | import { 3 | addAction, 4 | checkExpectations, 5 | checkInvariants, 6 | clearInvariants, 7 | describeUser, 8 | holdInvariants, 9 | setEvent, 10 | World 11 | } from './World'; 12 | import {Ganache} from 'eth-saddle/dist/config'; 13 | import Web3 from 'web3'; 14 | 15 | export async function forkWeb3(web3: Web3, url: string, accounts: string[]): Promise { 16 | let lastBlock = await web3.eth.getBlock("latest") 17 | return new Web3( 18 | Ganache.provider({ 19 | allowUnlimitedContractSize: true, 20 | fork: url, 21 | gasLimit: lastBlock.gasLimit, // maintain configured gas limit 22 | gasPrice: '20000', 23 | port: 8546, 24 | unlocked_accounts: accounts 25 | }) 26 | ); 27 | } 28 | 29 | export async function fork(world: World, url: string, accounts: string[]): Promise { 30 | let newWeb3 = await forkWeb3(world.web3, url, accounts); 31 | const newAccounts = loadAccounts(await newWeb3.eth.getAccounts()); 32 | 33 | return world 34 | .set('web3', newWeb3) 35 | .set('accounts', newAccounts); 36 | } 37 | -------------------------------------------------------------------------------- /tests/Contracts/ComptrollerScenario.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/Comptroller.sol"; 4 | 5 | contract ComptrollerScenario is Comptroller { 6 | uint public blockNumber; 7 | address compAddress; 8 | 9 | constructor() Comptroller() public {} 10 | 11 | function setCompAddress(address compAddress_) public { 12 | compAddress = compAddress_; 13 | } 14 | 15 | function getCompAddress() public view returns (address) { 16 | return compAddress; 17 | } 18 | 19 | function fastForward(uint blocks) public returns (uint) { 20 | blockNumber += blocks; 21 | return blockNumber; 22 | } 23 | 24 | function setBlockNumber(uint number) public { 25 | blockNumber = number; 26 | } 27 | 28 | function getBlockNumber() public view returns (uint) { 29 | return blockNumber; 30 | } 31 | 32 | function membershipLength(CToken cToken) public view returns (uint) { 33 | return accountAssets[address(cToken)].length; 34 | } 35 | 36 | function unlist(CToken cToken) public { 37 | markets[address(cToken)].isListed = false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/scenario/SetComptroller.scen: -------------------------------------------------------------------------------- 1 | -- Sets for `_setComptroller` Admin Function 2 | 3 | Test "Set Comptroller" 4 | NewComptroller 5 | NewCToken ZRX cZRX 6 | Assert Equal (CToken cZRX Comptroller) (Unitroller Address) 7 | ComptrollerImpl Deploy Scenario NewComptroller 8 | From Root (CToken cZRX SetComptroller (ComptrollerImpl NewComptroller Address)) 9 | -- TODO: Fix log assertion 10 | -- Assert Log "NewComptroller" ("oldComptroller" (Unitroller Address)) ("newComptroller" (ComptrollerImpl NewComptroller Address)) 11 | Assert Equal (CToken cZRX Comptroller) (ComptrollerImpl NewComptroller Address) 12 | 13 | Test "Fail when is not a comptroller" 14 | NewComptroller 15 | NewCToken ZRX cZRX 16 | Invariant Remains (CToken cZRX Comptroller) (Unitroller Address) 17 | AllowFailures 18 | From Root (CToken cZRX SetComptroller (PriceOracle Address)) 19 | Assert Revert 20 | 21 | Test "Fail to set comptroller as not admin" 22 | NewComptroller 23 | NewCToken ZRX cZRX 24 | AllowFailures 25 | From Geoff (CToken cZRX SetComptroller (PriceOracle Address)) 26 | Assert Failure UNAUTHORIZED SET_COMPTROLLER_OWNER_CHECK 27 | -------------------------------------------------------------------------------- /scenario/src/Invariant/StaticInvariant.ts: -------------------------------------------------------------------------------- 1 | import {Invariant} from '../Invariant'; 2 | import {fail, World} from '../World'; 3 | import {getCoreValue} from '../CoreValue'; 4 | import {Value} from '../Value'; 5 | import {Event} from '../Event'; 6 | import {formatEvent} from '../Formatter'; 7 | 8 | export class StaticInvariant implements Invariant { 9 | condition: Event; 10 | value: Value; 11 | held = false; 12 | 13 | constructor(condition: Event, value: Value) { 14 | this.condition = condition; 15 | this.value = value; 16 | } 17 | 18 | async getCurrentValue(world: World): Promise { 19 | return await getCoreValue(world, this.condition); 20 | }; 21 | 22 | async checker(world: World): Promise { 23 | const currentValue = await this.getCurrentValue(world); 24 | 25 | if (!this.value.compareTo(world, currentValue)) { 26 | fail(world, `Static invariant broken! Expected ${this.toString()} to remain static value \`${this.value}\` but became \`${currentValue}\``); 27 | } 28 | } 29 | 30 | toString() { 31 | return `StaticInvariant: condition=${formatEvent(this.condition)}, value=${this.value.toString()}`; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scenario/src/Expectation/RemainsExpectation.ts: -------------------------------------------------------------------------------- 1 | import {Expectation} from '../Expectation'; 2 | import {fail, World} from '../World'; 3 | import {getCoreValue} from '../CoreValue'; 4 | import {Value} from '../Value'; 5 | import {Event} from '../Event'; 6 | import {formatEvent} from '../Formatter'; 7 | 8 | export class RemainsExpectation implements Expectation { 9 | condition: Event; 10 | value: Value; 11 | 12 | constructor(condition: Event, value: Value) { 13 | this.condition = condition; 14 | this.value = value; 15 | } 16 | 17 | async getCurrentValue(world: World): Promise { 18 | return await getCoreValue(world, this.condition); 19 | }; 20 | 21 | async checker(world: World, initialCheck: boolean=false): Promise { 22 | const currentValue = await this.getCurrentValue(world); 23 | 24 | if (!this.value.compareTo(world, currentValue)) { 25 | fail(world, `${this.toString()} failed as value ${initialCheck ? 'started as' : 'became'} \`${currentValue.toString()}\``); 26 | } 27 | } 28 | 29 | toString() { 30 | return `RemainsExpectation: condition=${formatEvent(this.condition)}, value=${this.value.toString()}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Contracts/CompHarness.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../../contracts/Governance/Comp.sol"; 5 | 6 | contract CompScenario is Comp { 7 | constructor(address account) Comp(account) public {} 8 | 9 | function transferScenario(address[] calldata destinations, uint256 amount) external returns (bool) { 10 | for (uint i = 0; i < destinations.length; i++) { 11 | address dst = destinations[i]; 12 | _transferTokens(msg.sender, dst, uint96(amount)); 13 | } 14 | return true; 15 | } 16 | 17 | function transferFromScenario(address[] calldata froms, uint256 amount) external returns (bool) { 18 | for (uint i = 0; i < froms.length; i++) { 19 | address from = froms[i]; 20 | _transferTokens(from, msg.sender, uint96(amount)); 21 | } 22 | return true; 23 | } 24 | 25 | function generateCheckpoints(uint count, uint offset) external { 26 | for (uint i = 1 + offset; i <= count + offset; i++) { 27 | checkpoints[msg.sender][numCheckpoints[msg.sender]++] = Checkpoint(uint32(i), uint96(i)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scenario/src/Value/MaximillionValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import {Maximillion} from '../Contract/Maximillion'; 4 | import { 5 | getAddressV 6 | } from '../CoreValue'; 7 | import { 8 | AddressV, 9 | Value 10 | } from '../Value'; 11 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 12 | import {getMaximillion} from '../ContractLookup'; 13 | 14 | export async function getMaximillionAddress(world: World, maximillion: Maximillion): Promise { 15 | return new AddressV(maximillion._address); 16 | } 17 | 18 | export function maximillionFetchers() { 19 | return [ 20 | new Fetcher<{maximillion: Maximillion}, AddressV>(` 21 | #### Address 22 | 23 | * "Maximillion Address" - Returns address of maximillion 24 | `, 25 | "Address", 26 | [new Arg("maximillion", getMaximillion, {implicit: true})], 27 | (world, {maximillion}) => getMaximillionAddress(world, maximillion) 28 | ) 29 | ]; 30 | } 31 | 32 | export async function getMaximillionValue(world: World, event: Event): Promise { 33 | return await getFetcherValue("Maximillion", maximillionFetchers(), world, event); 34 | } 35 | -------------------------------------------------------------------------------- /scenario/src/Invariant/RemainsInvariant.ts: -------------------------------------------------------------------------------- 1 | import {Invariant} from '../Invariant'; 2 | import {fail, World} from '../World'; 3 | import {getCoreValue} from '../CoreValue'; 4 | import {Value} from '../Value'; 5 | import {Event} from '../Event'; 6 | import {formatEvent} from '../Formatter'; 7 | 8 | export class RemainsInvariant implements Invariant { 9 | condition: Event; 10 | value: Value; 11 | held = false; 12 | 13 | constructor(condition: Event, value: Value) { 14 | this.condition = condition; 15 | this.value = value; 16 | } 17 | 18 | async getCurrentValue(world: World): Promise { 19 | return await getCoreValue(world, this.condition); 20 | }; 21 | 22 | async checker(world: World, initialCheck: boolean=false): Promise { 23 | const currentValue = await this.getCurrentValue(world); 24 | 25 | if (!this.value.compareTo(world, currentValue)) { 26 | fail(world, `Static invariant broken! Expected ${this.toString()} to remain static value \`${this.value}\` but ${initialCheck ? 'started as' : 'became'} \`${currentValue}\``); 27 | } 28 | } 29 | 30 | toString() { 31 | return `RemainsInvariant: condition=${formatEvent(this.condition)}, value=${this.value.toString()}`; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /script/saddle/contractSizer.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); 2 | const fs = require('fs'); 3 | 4 | (async () => { 5 | fs.readFile('.build/contracts.json', (err, data) => { 6 | if (err) throw err; 7 | let contracts = JSON.parse(data); 8 | contracts = contracts['contracts']; 9 | const limit = 24576; 10 | console.log(colors.green('The EIP-170 limit is', limit, 'bytes')); 11 | 12 | const result = new Map(); 13 | Object.keys(contracts).forEach(function(contractName) { 14 | // Exclude test contracts. 15 | if (contractName.startsWith('contracts/')) { 16 | contract = contracts[contractName]; 17 | bin = contract['bin'] 18 | const digits = bin.length; 19 | const bytes = digits / 2; 20 | if (bytes > 0) { 21 | contractName = contractName.split(':')[1]; 22 | result.set(contractName, bytes); 23 | } 24 | } 25 | }); 26 | 27 | const sortedResult = new Map([...result.entries()].sort((a, b) => a[1] - b[1])); 28 | sortedResult.forEach(function(size, name) { 29 | size = size.toString(); 30 | if (size > limit) { 31 | size = colors.red(size); 32 | } 33 | console.log('%s => %s bytes', name.padEnd(30), size); 34 | }); 35 | }); 36 | })(); 37 | -------------------------------------------------------------------------------- /scenario/src/Value/ComptrollerImplValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import {ComptrollerImpl} from '../Contract/ComptrollerImpl'; 4 | import { 5 | getAddressV 6 | } from '../CoreValue'; 7 | import { 8 | AddressV, 9 | Value 10 | } from '../Value'; 11 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 12 | import {getComptrollerImpl} from '../ContractLookup'; 13 | 14 | export async function getComptrollerImplAddress(world: World, comptrollerImpl: ComptrollerImpl): Promise { 15 | return new AddressV(comptrollerImpl._address); 16 | } 17 | 18 | export function comptrollerImplFetchers() { 19 | return [ 20 | new Fetcher<{comptrollerImpl: ComptrollerImpl}, AddressV>(` 21 | #### Address 22 | 23 | * "ComptrollerImpl Address" - Returns address of comptroller implementation 24 | `, 25 | "Address", 26 | [new Arg("comptrollerImpl", getComptrollerImpl)], 27 | (world, {comptrollerImpl}) => getComptrollerImplAddress(world, comptrollerImpl), 28 | {namePos: 1} 29 | ) 30 | ]; 31 | } 32 | 33 | export async function getComptrollerImplValue(world: World, event: Event): Promise { 34 | return await getFetcherValue("ComptrollerImpl", comptrollerImplFetchers(), world, event); 35 | } 36 | -------------------------------------------------------------------------------- /scenario/src/Contract/PriceOracle.ts: -------------------------------------------------------------------------------- 1 | import {Contract} from '../Contract'; 2 | import {Callable, Sendable} from '../Invokation'; 3 | import {encodedNumber} from '../Encoding'; 4 | 5 | interface PriceOracleMethods { 6 | assetPrices(asset: string): Callable 7 | setUnderlyingPrice(cToken: string, amount: encodedNumber): Sendable 8 | setDirectPrice(address: string, amount: encodedNumber): Sendable 9 | 10 | // Anchor Price Oracle 11 | getPrice(asset: string): Callable 12 | readers(asset: string): Callable 13 | anchorAdmin(): Callable 14 | pendingAnchorAdmin(): Callable 15 | poster(): Callable 16 | maxSwing(): Callable 17 | anchors(asset: string): Callable<{0: number, 1: number}> 18 | pendingAnchors(asset: string): Callable 19 | _setPendingAnchor(asset: string, price: encodedNumber): Sendable 20 | _setPaused(paused: boolean): Sendable 21 | _setPendingAnchorAdmin(string): Sendable 22 | _acceptAnchorAdmin(): Sendable 23 | setPrice(asset: string, price: encodedNumber): Sendable 24 | setPrices(assets: string[], prices: encodedNumber[]): Sendable 25 | } 26 | 27 | export interface PriceOracle extends Contract { 28 | methods: PriceOracleMethods 29 | name: string 30 | } 31 | -------------------------------------------------------------------------------- /script/build_scenarios: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | root_dir="$(cd $dir/.. && pwd)" 7 | tests_dir="$(cd $root_dir/tests && pwd)" 8 | scenario_dir="$tests_dir/Scenarios" 9 | cache_file="$root_dir/.scencache" 10 | checksum="$(echo $root_dir/spec/scenario{,/**}/*.scen | xargs shasum | shasum | cut -d' ' -f 1)" 11 | 12 | if [ -z "$rebuild" -a -f "$cache_file" ]; then 13 | cached="$(cat $cache_file)" 14 | 15 | if [ "$cached" == "$checksum" ]; then 16 | echo "Skipping Scenario Stub Rebuild (set rebuild=true to force)" 17 | exit 0 18 | fi 19 | fi 20 | 21 | echo "Build scenario stubs..." 22 | rm -rf "$scenario_dir" && mkdir "$scenario_dir" 23 | 24 | scenario_test="$(cat <<-EOF 25 | const scenario = require('REL_PATHScenario'); 26 | 27 | scenario.run('SCEN_FILE'); 28 | EOF 29 | )" 30 | 31 | cd $root_dir/spec/scenario 32 | 33 | for scenario in .{,/**}/*.scen; do 34 | base="${scenario#.\/}" 35 | filename="${base%.*}ScenTest.js" 36 | final="$scenario_dir/$filename" 37 | mkdir -p "$(dirname "$final")" 38 | rel_path="$(echo "$scenario" | sed 's$[^/]$$g' | sed 's$/$../$g')" 39 | echo "$scenario_test" | sed "s^SCEN_FILE^$base^g" | sed "s^REL_PATH^$rel_path^g" > "$final" 40 | done 41 | 42 | echo "$checksum" > "$cache_file" 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compound-protocol", 3 | "version": "0.2.1", 4 | "description": "The Compound Money Market", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "./script/compile", 8 | "console": "if node -v | grep -E \"v(12|13)\" > /dev/null; then flags=\"-n --experimental-repl-await\"; fi; npx $flags saddle console", 9 | "coverage": "./script/coverage", 10 | "deploy": "./scenario/script/repl -s ./scenario/scen/deploy.scen", 11 | "lint": "./script/lint", 12 | "repl": "./scenario/script/repl", 13 | "profile": "yarn test tests/gasProfiler.js", 14 | "test": "./script/test", 15 | "test:prepare": "NO_RUN=true ./script/test" 16 | }, 17 | "repository": "git@github.com:compound-finance/compound-protocol.git", 18 | "author": "Compound Finance", 19 | "license": "UNLICENSED", 20 | "devDependencies": { 21 | "bignumber.js": "8.0.1", 22 | "jest-diff": "^24.9.0", 23 | "jest-junit": "^6.4.0", 24 | "solium": "^1.2.5", 25 | "solparse": "^2.2.8" 26 | }, 27 | "dependencies": { 28 | "eth-saddle": "^0.1.17" 29 | }, 30 | "resolutions": { 31 | "scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", 32 | "**/ganache-core": "https://github.com/compound-finance/ganache-core.git#jflatow/unbreak-fork" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/sim/0003-borrow-caps-patch/hypothetical_upgrade_post_propose.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn repl -s 2 | 3 | PrintTransactionLogs 4 | Alias CompVoter1 "0x9aa835bc7b8ce13b9b0c9764a52fbf71ac62ccf1" 5 | Alias CompVoter2 "0xed409c9ff60f3020abf9012bcd45fc294f5608ff" 6 | Alias USDCWhale "0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3" 7 | Alias Arr00 "0x2b384212edc04ae8bb41738d05ba20e33277bf33" 8 | Web3Fork "https://mainnet-eth.compound.finance/@10823813" (USDCWhale CompVoter1 CompVoter2) 9 | UseConfigs mainnet 10 | 11 | 12 | 13 | -- Vote for, queue, and execute the proposal 14 | 15 | MineBlock 16 | From CompVoter1 (Governor GovernorAlpha Proposal LastProposal Vote For) 17 | From CompVoter2 (Governor GovernorAlpha Proposal LastProposal Vote For) 18 | AdvanceBlocks 20000 19 | Governor GovernorAlpha Proposal LastProposal Queue 20 | IncreaseTime 604910 21 | Governor GovernorAlpha Proposal LastProposal Execute 22 | ComptrollerImpl StdComptrollerG5 MergeABI 23 | 24 | Assert Equal (Address (Unitroller Implementation)) (Address StdComptrollerG5) 25 | Assert Equal (Erc20 SAI TokenBalance (Address Arr00)) (2800000000000000000000) 26 | 27 | From USDCWhale (Trx GasPrice 0 (Erc20 USDC Approve cUSDC UInt256Max)) 28 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Mint 100000000e6)) 29 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 1000000e6)) 30 | 31 | 32 | Print "Borrow Limit Comptroller Patch OK!" -------------------------------------------------------------------------------- /spec/certora/contracts/CErc20DelegateCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/CErc20Delegate.sol"; 4 | import "../../../contracts/EIP20Interface.sol"; 5 | 6 | import "./CTokenCollateral.sol"; 7 | 8 | contract CErc20DelegateCertora is CErc20Delegate { 9 | CTokenCollateral public otherToken; 10 | 11 | function mintFreshPub(address minter, uint mintAmount) public returns (uint) { 12 | (uint error,) = mintFresh(minter, mintAmount); 13 | return error; 14 | } 15 | 16 | function redeemFreshPub(address payable redeemer, uint redeemTokens, uint redeemUnderlying) public returns (uint) { 17 | return redeemFresh(redeemer, redeemTokens, redeemUnderlying); 18 | } 19 | 20 | function borrowFreshPub(address payable borrower, uint borrowAmount) public returns (uint) { 21 | return borrowFresh(borrower, borrowAmount); 22 | } 23 | 24 | function repayBorrowFreshPub(address payer, address borrower, uint repayAmount) public returns (uint) { 25 | (uint error,) = repayBorrowFresh(payer, borrower, repayAmount); 26 | return error; 27 | } 28 | 29 | function liquidateBorrowFreshPub(address liquidator, address borrower, uint repayAmount) public returns (uint) { 30 | (uint error,) = liquidateBorrowFresh(liquidator, borrower, repayAmount, otherToken); 31 | return error; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/certora/contracts/CompCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/Governance/Comp.sol"; 4 | 5 | contract CompCertora is Comp { 6 | constructor(address grantor) Comp(grantor) public {} 7 | 8 | function certoraOrdered(address account) external view returns (bool) { 9 | uint32 nCheckpoints = numCheckpoints[account]; 10 | for (uint32 i = 1; i < nCheckpoints; i++) { 11 | if (checkpoints[account][i - 1].fromBlock >= checkpoints[account][i].fromBlock) { 12 | return false; 13 | } 14 | } 15 | 16 | // make sure the checkpoints are also all before the current block 17 | if (nCheckpoints > 0 && checkpoints[account][nCheckpoints - 1].fromBlock > block.number) { 18 | return false; 19 | } 20 | 21 | return true; 22 | } 23 | 24 | function certoraScan(address account, uint blockNumber) external view returns (uint) { 25 | // find most recent checkpoint from before blockNumber 26 | for (uint32 i = numCheckpoints[account]; i != 0; i--) { 27 | Checkpoint memory cp = checkpoints[account][i-1]; 28 | if (cp.fromBlock <= blockNumber) { 29 | return cp.votes; 30 | } 31 | } 32 | 33 | // blockNumber is from before first checkpoint (or list is empty) 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scenario/src/Contract/Timelock.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { encodedNumber } from '../Encoding'; 4 | 5 | interface TimelockMethods { 6 | admin(): Callable; 7 | pendingAdmin(): Callable; 8 | delay(): Callable; 9 | queuedTransactions(txHash: string): Callable; 10 | setDelay(delay: encodedNumber): Sendable; 11 | acceptAdmin(): Sendable; 12 | setPendingAdmin(admin: string): Sendable; 13 | queueTransaction( 14 | target: string, 15 | value: encodedNumber, 16 | signature: string, 17 | data: string, 18 | eta: encodedNumber 19 | ): Sendable; 20 | cancelTransaction( 21 | target: string, 22 | value: encodedNumber, 23 | signature: string, 24 | data: string, 25 | eta: encodedNumber 26 | ): Sendable; 27 | executeTransaction( 28 | target: string, 29 | value: encodedNumber, 30 | signature: string, 31 | data: string, 32 | eta: encodedNumber 33 | ): Sendable; 34 | 35 | blockTimestamp(): Callable; 36 | harnessFastForward(seconds: encodedNumber): Sendable; 37 | harnessSetBlockTimestamp(seconds: encodedNumber): Sendable; 38 | harnessSetAdmin(admin: string): Sendable; 39 | } 40 | 41 | export interface Timelock extends Contract { 42 | methods: TimelockMethods; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/CErc20Delegate.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./CErc20.sol"; 4 | 5 | /** 6 | * @title Compound's CErc20Delegate Contract 7 | * @notice CTokens which wrap an EIP-20 underlying and are delegated to 8 | * @author Compound 9 | */ 10 | contract CErc20Delegate is CErc20, CDelegateInterface { 11 | /** 12 | * @notice Construct an empty delegate 13 | */ 14 | constructor() public {} 15 | 16 | /** 17 | * @notice Called by the delegator on a delegate to initialize it for duty 18 | * @param data The encoded bytes data for any initialization 19 | */ 20 | function _becomeImplementation(bytes memory data) public { 21 | // Shh -- currently unused 22 | data; 23 | 24 | // Shh -- we don't ever want this hook to be marked pure 25 | if (false) { 26 | implementation = address(0); 27 | } 28 | 29 | require(msg.sender == admin, "only the admin may call _becomeImplementation"); 30 | } 31 | 32 | /** 33 | * @notice Called by the delegator on a delegate to forfeit its responsibility 34 | */ 35 | function _resignImplementation() public { 36 | // Shh -- we don't ever want this hook to be marked pure 37 | if (false) { 38 | implementation = address(0); 39 | } 40 | 41 | require(msg.sender == admin, "only the admin may call _resignImplementation"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Contracts/InterestRateModelHarness.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/InterestRateModel.sol"; 4 | 5 | /** 6 | * @title An Interest Rate Model for tests that can be instructed to return a failure instead of doing a calculation 7 | * @author Compound 8 | */ 9 | contract InterestRateModelHarness is InterestRateModel { 10 | uint public constant opaqueBorrowFailureCode = 20; 11 | bool public failBorrowRate; 12 | uint public borrowRate; 13 | 14 | constructor(uint borrowRate_) public { 15 | borrowRate = borrowRate_; 16 | } 17 | 18 | function setFailBorrowRate(bool failBorrowRate_) public { 19 | failBorrowRate = failBorrowRate_; 20 | } 21 | 22 | function setBorrowRate(uint borrowRate_) public { 23 | borrowRate = borrowRate_; 24 | } 25 | 26 | function getBorrowRate(uint _cash, uint _borrows, uint _reserves) public view returns (uint) { 27 | _cash; // unused 28 | _borrows; // unused 29 | _reserves; // unused 30 | require(!failBorrowRate, "INTEREST_RATE_MODEL_ERROR"); 31 | return borrowRate; 32 | } 33 | 34 | function getSupplyRate(uint _cash, uint _borrows, uint _reserves, uint _reserveFactor) external view returns (uint) { 35 | _cash; // unused 36 | _borrows; // unused 37 | _reserves; // unused 38 | return borrowRate * (1 - _reserveFactor); 39 | } 40 | } -------------------------------------------------------------------------------- /spec/certora/CErc20/rounding.cvl: -------------------------------------------------------------------------------- 1 | 2 | rule redeemRounding(uint256 redeemTokensIn, uint256 redeemAmountIn) 3 | description "Redeemer can withdraw tokens for free" { 4 | env e0; 5 | env e1; 6 | require e1.block.number >= e0.block.number; 7 | 8 | uint256 exchangeRate = sinvoke exchangeRateStored(e0); 9 | require exchangeRate > 0; 10 | uint256 redeemTokens = redeemAmountIn / exchangeRate / 1000000000000000000; 11 | uint256 redeemAmount = redeemTokensIn * exchangeRate / 1000000000000000000; 12 | bool tokensTruncated = redeemAmountIn > 0 && redeemTokens == 0; 13 | bool amountTruncated = redeemTokensIn > 0 && redeemAmount == 0; 14 | 15 | address redeemer = e0.msg.sender; 16 | 17 | uint256 preTokens = sinvoke balanceOf(e0, redeemer); 18 | uint256 preCash = sinvoke getCash(e0); 19 | 20 | uint256 error = sinvoke redeemFreshPub(e0, redeemer, redeemTokensIn, redeemAmountIn); 21 | bool redeemReverted = lastReverted; 22 | bool redeemSucceeded = !redeemReverted && error == 0; 23 | 24 | uint256 postTokens = sinvoke balanceOf(e1, redeemer); 25 | uint256 postCash = sinvoke getCash(e1); 26 | 27 | assert (redeemSucceeded => (!tokensTruncated <=> (postCash < preCash => postTokens < preTokens))), "Cash decreased but redeemer tokens did not"; 28 | assert (redeemSucceeded => (!amountTruncated <=> (postTokens < preTokens => postCash < preCash))), "Redeemer tokens decreased but cash did not"; 29 | } -------------------------------------------------------------------------------- /scenario/src/File.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { World } from './World'; 4 | 5 | export function getNetworkPath(basePath: string | null, network: string, name: string, extension: string | null='json'): string { 6 | return path.join(basePath || '', 'networks', `${network}${name}${extension ? `.${extension}` : ''}`); 7 | } 8 | 9 | export async function readFile(world: World | null, file: string, def: T, fn: (data: string) => T): Promise { 10 | if (world && world.fs) { 11 | let data = world.fs[file]; 12 | return Promise.resolve(data ? fn(data) : def); 13 | } else { 14 | return new Promise((resolve, reject) => { 15 | fs.access(file, fs.constants.F_OK, (err) => { 16 | if (err) { 17 | resolve(def); 18 | } else { 19 | fs.readFile(file, 'utf8', (err, data) => { 20 | return err ? reject(err) : resolve(fn(data)); 21 | }); 22 | } 23 | }); 24 | }); 25 | } 26 | } 27 | 28 | export async function writeFile(world: World | null, file: string, data: string): Promise { 29 | if (world && world.fs) { 30 | world = world.setIn(['fs', file], data); 31 | return Promise.resolve(world); 32 | } else { 33 | return new Promise((resolve, reject) => { 34 | fs.writeFile(file, data, (err) => { 35 | return err ? reject(err) : resolve(world!); // XXXS `!` 36 | }); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /script/saddle/matchToken.js: -------------------------------------------------------------------------------- 1 | let { loadAddress, loadConf } = require('./support/tokenConfig'); 2 | 3 | function printUsage() { 4 | console.log(` 5 | usage: npx saddle script token:match address {tokenConfig} 6 | 7 | This checks to see if the deployed byte-code matches this version of the Compound Protocol. 8 | 9 | example: 10 | 11 | npx saddle -n rinkeby script token:match 0x19B674715cD20626415C738400FDd0d32D6809B6 '{ 12 | "underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A", 13 | "comptroller": "$Comptroller", 14 | "interestRateModel": "$Base200bps_Slope3000bps", 15 | "initialExchangeRateMantissa": "2.0e18", 16 | "name": "Compound Kyber Network Crystal", 17 | "symbol": "cKNC", 18 | "decimals": "8", 19 | "admin": "$Timelock" 20 | }' 21 | `); 22 | } 23 | 24 | (async function() { 25 | if (args.length !== 2) { 26 | return printUsage(); 27 | } 28 | 29 | let address = loadAddress(args[0], addresses); 30 | let conf = loadConf(args[1], addresses); 31 | if (!conf) { 32 | return printUsage(); 33 | } 34 | 35 | console.log(`Matching cToken at ${address} with ${JSON.stringify(conf)}`); 36 | 37 | let deployArgs = [conf.underlying, conf.comptroller, conf.interestRateModel, conf.initialExchangeRateMantissa.toString(), conf.name, conf.symbol, conf.decimals, conf.admin]; 38 | 39 | await saddle.match(address, 'CErc20Immutable', deployArgs); 40 | 41 | return { 42 | ...conf, 43 | address 44 | }; 45 | })(); 46 | -------------------------------------------------------------------------------- /scenario/script/repl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | dir=`dirname $0` 6 | tsc_root="$dir/.." 7 | proj_root="$dir/../.." 8 | networks_root="$dir/../../networks" 9 | network=${NETWORK:-development} 10 | script=() 11 | verbose="$VERBOSE" 12 | dry_run="$DRY_RUN" 13 | no_tsc="$NO_TSC" 14 | 15 | [ -n "$SCRIPT" ] && script+=("$SCRIPT") 16 | 17 | usage() { echo "$0 usage:" && grep ".)\ #" $0; exit 0; } 18 | while getopts ":hdn:e:s:vt" arg; do 19 | case $arg in 20 | c) # Don't compile 21 | no_compile="true" 22 | ;; 23 | d) # Dry run 24 | dry_run="true" 25 | ;; 26 | h) # Hypothetical 27 | hypothetical="true" 28 | ;; 29 | e) # Add variables for script (key=value,key2=value2) 30 | env_vars="$OPTARG" 31 | ;; 32 | n) # Specify network 33 | network=$OPTARG 34 | ;; 35 | s) # Specify a script to run 36 | [ ! -f "$OPTARG" ] \ 37 | && echo "Cannot find script $OPTARG" \ 38 | && exit 1 39 | script+=("$OPTARG") 40 | ;; 41 | t) # Don't build TSC 42 | no_tsc="true" 43 | ;; 44 | 45 | v) # Verbose 46 | verbose="true" 47 | ;; 48 | 49 | h | *) # Display help. 50 | usage 51 | exit 0 52 | ;; 53 | esac 54 | done 55 | 56 | [[ -z $no_tsc ]] && "$dir/tsc" 57 | [[ -z $no_compile ]] && "$proj_root/script/compile" 58 | 59 | proj_root="$proj_root" env_vars="$env_vars" dry_run="$dry_run" script="$(IFS=, ; echo "${script[*]}")" network="$network" verbose="$verbose" node "$tsc_root/.tsbuilt/repl.js" 60 | -------------------------------------------------------------------------------- /contracts/InterestRateModel.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | /** 4 | * @title Compound's InterestRateModel Interface 5 | * @author Compound 6 | */ 7 | contract InterestRateModel { 8 | /// @notice Indicator that this is an InterestRateModel contract (for inspection) 9 | bool public constant isInterestRateModel = true; 10 | 11 | /** 12 | * @notice Calculates the current borrow interest rate per block 13 | * @param cash The total amount of cash the market has 14 | * @param borrows The total amount of borrows the market has outstanding 15 | * @param reserves The total amnount of reserves the market has 16 | * @return The borrow rate per block (as a percentage, and scaled by 1e18) 17 | */ 18 | function getBorrowRate(uint cash, uint borrows, uint reserves) external view returns (uint); 19 | 20 | /** 21 | * @notice Calculates the current supply interest rate per block 22 | * @param cash The total amount of cash the market has 23 | * @param borrows The total amount of borrows the market has outstanding 24 | * @param reserves The total amnount of reserves the market has 25 | * @param reserveFactorMantissa The current reserve factor the market has 26 | * @return The supply rate per block (as a percentage, and scaled by 1e18) 27 | */ 28 | function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) external view returns (uint); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Compound Labs, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /tests/Contracts/EvilToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./FaucetToken.sol"; 4 | 5 | /** 6 | * @title The Compound Evil Test Token 7 | * @author Compound 8 | * @notice A simple test token that fails certain operations 9 | */ 10 | contract EvilToken is FaucetToken { 11 | bool public fail; 12 | 13 | constructor(uint256 _initialAmount, string memory _tokenName, uint8 _decimalUnits, string memory _tokenSymbol) public 14 | FaucetToken(_initialAmount, _tokenName, _decimalUnits, _tokenSymbol) { 15 | fail = true; 16 | } 17 | 18 | function setFail(bool _fail) external { 19 | fail = _fail; 20 | } 21 | 22 | function transfer(address dst, uint256 amount) external returns (bool) { 23 | if (fail) { 24 | return false; 25 | } 26 | balanceOf[msg.sender] = balanceOf[msg.sender].sub(amount); 27 | balanceOf[dst] = balanceOf[dst].add(amount); 28 | emit Transfer(msg.sender, dst, amount); 29 | return true; 30 | } 31 | 32 | function transferFrom(address src, address dst, uint256 amount) external returns (bool) { 33 | if (fail) { 34 | return false; 35 | } 36 | balanceOf[src] = balanceOf[src].sub(amount); 37 | balanceOf[dst] = balanceOf[dst].add(amount); 38 | allowance[src][msg.sender] = allowance[src][msg.sender].sub(amount); 39 | emit Transfer(src, dst, amount); 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/SimplePriceOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./PriceOracle.sol"; 4 | import "./CErc20.sol"; 5 | 6 | contract SimplePriceOracle is PriceOracle { 7 | mapping(address => uint) prices; 8 | event PricePosted(address asset, uint previousPriceMantissa, uint requestedPriceMantissa, uint newPriceMantissa); 9 | 10 | function getUnderlyingPrice(CToken cToken) public view returns (uint) { 11 | if (compareStrings(cToken.symbol(), "cETH")) { 12 | return 1e18; 13 | } else { 14 | return prices[address(CErc20(address(cToken)).underlying())]; 15 | } 16 | } 17 | 18 | function setUnderlyingPrice(CToken cToken, uint underlyingPriceMantissa) public { 19 | address asset = address(CErc20(address(cToken)).underlying()); 20 | emit PricePosted(asset, prices[asset], underlyingPriceMantissa, underlyingPriceMantissa); 21 | prices[asset] = underlyingPriceMantissa; 22 | } 23 | 24 | function setDirectPrice(address asset, uint price) public { 25 | emit PricePosted(asset, prices[asset], price, price); 26 | prices[asset] = price; 27 | } 28 | 29 | // v1 price oracle interface for use as backing of proxy 30 | function assetPrices(address asset) external view returns (uint) { 31 | return prices[asset]; 32 | } 33 | 34 | function compareStrings(string memory a, string memory b) internal pure returns (bool) { 35 | return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scenario/src/Encoding.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { utils, ethers } from 'ethers'; 3 | 4 | const smallEnoughNumber = new BigNumber('100000000'); 5 | 6 | export type encodedNumber = number | utils.BigNumber; 7 | 8 | // Returns the mantissa of an Exp with given floating value 9 | export function getExpMantissa(float: number): encodedNumber { 10 | // Workaround from https://github.com/ethereum/web3.js/issues/1920 11 | const str = Math.floor(float * 1.0e18).toString(); 12 | 13 | return toEncodableNum(str); 14 | } 15 | 16 | export function toEncodableNum(amountArgRaw: string | encodedNumber): encodedNumber { 17 | let bigNumber; 18 | if (amountArgRaw instanceof BigNumber) { 19 | bigNumber = amountArgRaw; 20 | } else { 21 | bigNumber = new BigNumber(amountArgRaw.toString()); 22 | } 23 | 24 | if (bigNumber.lt(smallEnoughNumber)) { 25 | // The Ethers abi encoder can handle regular numbers (including with fractional part) 26 | // and its own internal big number class which is different from BigNumber.js published on npm (and can't accept 27 | // fractional parts.) 28 | // If the input is not huge, we just use a number, otherwise we try to use the Ethers class. 29 | 30 | return Number(amountArgRaw); 31 | } else { 32 | // bigNumberify (and the result class) only accept integers as digits, so we do .toFixed() to convert, for example, 1e4 to 10000. 33 | // Rather than doing toFixed(0) and silently truncating a fractional part, we'll let it through and get an error. 34 | // that case 35 | return utils.bigNumberify(bigNumber.toFixed()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Contracts/Fauceteer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/EIP20NonStandardInterface.sol"; 4 | 5 | /** 6 | * @title Fauceteer 7 | * @author Compound 8 | * @notice First computer program to be part of The Giving Pledge 9 | */ 10 | contract Fauceteer { 11 | 12 | /** 13 | * @notice Drips some tokens to caller 14 | * @dev We send 0.01% of our tokens to the caller. Over time, the amount will tend toward and eventually reach zero. 15 | * @param token The token to drip. Note: if we have no balance in this token, function will revert. 16 | */ 17 | function drip(EIP20NonStandardInterface token) public { 18 | uint balance = token.balanceOf(address(this)); 19 | require(balance > 0, "Fauceteer is empty"); 20 | token.transfer(msg.sender, balance / 10000); // 0.01% 21 | 22 | bool success; 23 | assembly { 24 | switch returndatasize() 25 | case 0 { // This is a non-standard ERC-20 26 | success := not(0) // set success to true 27 | } 28 | case 32 { // This is a compliant ERC-20 29 | returndatacopy(0, 0, 32) 30 | success := mload(0) // Set `success = returndata` of external call 31 | } 32 | default { // This is an excessively non-compliant ERC-20, revert. 33 | revert(0, 0) 34 | } 35 | } 36 | 37 | require(success, "Transfer returned false."); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Governance/CompScenarioTest.js: -------------------------------------------------------------------------------- 1 | 2 | describe('CompScenario', () => { 3 | let root, accounts; 4 | let comp; 5 | 6 | beforeEach(async () => { 7 | [root, ...accounts] = saddle.accounts; 8 | comp = await deploy('CompScenario', [root]); 9 | }); 10 | 11 | describe('lookup curve', () => { 12 | [ 13 | [1, 3], 14 | [2, 5], 15 | [20, 8], 16 | [100, 10], 17 | [500, 12], 18 | ...(process.env['SLOW'] ? [ [5000, 16], [20000, 18] ] : []) 19 | ].forEach(([checkpoints, expectedReads]) => { 20 | it(`with ${checkpoints} checkpoints, has ${expectedReads} reads`, async () => { 21 | let remaining = checkpoints; 22 | let offset = 0; 23 | while (remaining > 0) { 24 | let amt = remaining > 1000 ? 1000 : remaining; 25 | await comp.methods.generateCheckpoints(amt, offset).send({from: root, gas: 200000000}); 26 | remaining -= amt; 27 | offset += amt; 28 | } 29 | 30 | let result = await comp.methods.getPriorVotes(root, 1).send(); 31 | 32 | await saddle.trace(result, { 33 | constants: { 34 | "account": root 35 | }, 36 | preFilter: ({op}) => op === 'SLOAD', 37 | postFilter: ({source}) => !source || !source.includes('mockBlockNumber'), 38 | execLog: (log) => { 39 | if (process.env['VERBOSE']) { 40 | log.show(); 41 | } 42 | }, 43 | exec: (logs, info) => { 44 | expect(logs.length).toEqual(expectedReads); 45 | } 46 | }); 47 | }, 600000); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /contracts/CErc20Immutable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./CErc20.sol"; 4 | 5 | /** 6 | * @title Compound's CErc20Immutable Contract 7 | * @notice CTokens which wrap an EIP-20 underlying and are immutable 8 | * @author Compound 9 | */ 10 | contract CErc20Immutable is CErc20 { 11 | /** 12 | * @notice Construct a new money market 13 | * @param underlying_ The address of the underlying asset 14 | * @param comptroller_ The address of the Comptroller 15 | * @param interestRateModel_ The address of the interest rate model 16 | * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 17 | * @param name_ ERC-20 name of this token 18 | * @param symbol_ ERC-20 symbol of this token 19 | * @param decimals_ ERC-20 decimal precision of this token 20 | * @param admin_ Address of the administrator of this token 21 | */ 22 | constructor(address underlying_, 23 | ComptrollerInterface comptroller_, 24 | InterestRateModel interestRateModel_, 25 | uint initialExchangeRateMantissa_, 26 | string memory name_, 27 | string memory symbol_, 28 | uint8 decimals_, 29 | address payable admin_) public { 30 | // Creator of the contract is admin during initialization 31 | admin = msg.sender; 32 | 33 | // Initialize the market 34 | initialize(underlying_, comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); 35 | 36 | // Set the proper admin now that initialization is done 37 | admin = admin_; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scenario/src/Contract/Comp.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { encodedNumber } from '../Encoding'; 3 | import { Callable, Sendable } from '../Invokation'; 4 | 5 | interface Checkpoint { 6 | fromBlock: number; 7 | votes: number; 8 | } 9 | 10 | export interface CompMethods { 11 | name(): Callable; 12 | symbol(): Callable; 13 | decimals(): Callable; 14 | totalSupply(): Callable; 15 | balanceOf(address: string): Callable; 16 | allowance(owner: string, spender: string): Callable; 17 | approve(address: string, amount: encodedNumber): Sendable; 18 | transfer(address: string, amount: encodedNumber): Sendable; 19 | transferFrom(owner: string, spender: string, amount: encodedNumber): Sendable; 20 | checkpoints(account: string, index: number): Callable; 21 | numCheckpoints(account: string): Callable; 22 | delegate(account: string): Sendable; 23 | getCurrentVotes(account: string): Callable; 24 | getPriorVotes(account: string, blockNumber: encodedNumber): Callable; 25 | setBlockNumber(blockNumber: encodedNumber): Sendable; 26 | } 27 | 28 | export interface CompScenarioMethods extends CompMethods { 29 | transferScenario(destinations: string[], amount: encodedNumber): Sendable; 30 | transferFromScenario(froms: string[], amount: encodedNumber): Sendable; 31 | } 32 | 33 | export interface Comp extends Contract { 34 | methods: CompMethods; 35 | name: string; 36 | } 37 | 38 | export interface CompScenario extends Contract { 39 | methods: CompScenarioMethods; 40 | name: string; 41 | } 42 | -------------------------------------------------------------------------------- /tests/Contracts/ComptrollerScenarioG1.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/ComptrollerG1.sol"; 4 | import "../../contracts/PriceOracle.sol"; 5 | 6 | // XXX we should delete G1 everything... 7 | // requires fork/deploy bytecode tests 8 | 9 | contract ComptrollerScenarioG1 is ComptrollerG1 { 10 | uint public blockNumber; 11 | 12 | constructor() ComptrollerG1() public {} 13 | 14 | function membershipLength(CToken cToken) public view returns (uint) { 15 | return accountAssets[address(cToken)].length; 16 | } 17 | 18 | function fastForward(uint blocks) public returns (uint) { 19 | blockNumber += blocks; 20 | 21 | return blockNumber; 22 | } 23 | 24 | function setBlockNumber(uint number) public { 25 | blockNumber = number; 26 | } 27 | 28 | function _become( 29 | Unitroller unitroller, 30 | PriceOracle _oracle, 31 | uint _closeFactorMantissa, 32 | uint _maxAssets, 33 | bool reinitializing) public { 34 | super._become(unitroller, _oracle, _closeFactorMantissa, _maxAssets, reinitializing); 35 | } 36 | 37 | function getHypotheticalAccountLiquidity( 38 | address account, 39 | address cTokenModify, 40 | uint redeemTokens, 41 | uint borrowAmount) public view returns (uint, uint, uint) { 42 | (Error err, uint liquidity, uint shortfall) = 43 | super.getHypotheticalAccountLiquidityInternal(account, CToken(cTokenModify), redeemTokens, borrowAmount); 44 | return (uint(err), liquidity, shortfall); 45 | } 46 | 47 | function unlist(CToken cToken) public { 48 | markets[address(cToken)].isListed = false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Contracts/FeeToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./FaucetToken.sol"; 4 | 5 | /** 6 | * @title Fee Token 7 | * @author Compound 8 | * @notice A simple test token that charges fees on transfer. Used to mock USDT. 9 | */ 10 | contract FeeToken is FaucetToken { 11 | uint public basisPointFee; 12 | address public owner; 13 | 14 | constructor( 15 | uint256 _initialAmount, 16 | string memory _tokenName, 17 | uint8 _decimalUnits, 18 | string memory _tokenSymbol, 19 | uint _basisPointFee, 20 | address _owner 21 | ) FaucetToken(_initialAmount, _tokenName, _decimalUnits, _tokenSymbol) public { 22 | basisPointFee = _basisPointFee; 23 | owner = _owner; 24 | } 25 | 26 | function transfer(address dst, uint amount) public returns (bool) { 27 | uint fee = amount.mul(basisPointFee).div(10000); 28 | uint net = amount.sub(fee); 29 | balanceOf[owner] = balanceOf[owner].add(fee); 30 | balanceOf[msg.sender] = balanceOf[msg.sender].sub(amount); 31 | balanceOf[dst] = balanceOf[dst].add(net); 32 | emit Transfer(msg.sender, dst, amount); 33 | return true; 34 | } 35 | 36 | function transferFrom(address src, address dst, uint amount) public returns (bool) { 37 | uint fee = amount.mul(basisPointFee).div(10000); 38 | uint net = amount.sub(fee); 39 | balanceOf[owner] = balanceOf[owner].add(fee); 40 | balanceOf[src] = balanceOf[src].sub(amount); 41 | balanceOf[dst] = balanceOf[dst].add(net); 42 | allowance[src][msg.sender] = allowance[src][msg.sender].sub(amount); 43 | emit Transfer(src, dst, amount); 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * This module loads Error and FailureInfo enum from ErrorReporter.sol. 5 | */ 6 | 7 | const path = require('path'); 8 | const solparse = require('solparse'); 9 | 10 | const errorReporterPath = path.join(__dirname, '..', 'contracts', 'ErrorReporter.sol'); 11 | const contents = solparse.parseFile(errorReporterPath); 12 | const [ 13 | ComptrollerErrorReporter, 14 | TokenErrorReporter 15 | ] = contents.body.filter(k => k.type === 'ContractStatement'); 16 | 17 | function invert(object) { 18 | return Object.entries(object).reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {}); 19 | } 20 | 21 | function parse(reporter) { 22 | const ErrorInv = reporter.body.find(k => k.name == 'Error').members; 23 | const FailureInfoInv = reporter.body.find(k => k.name == 'FailureInfo').members; 24 | const Error = invert(ErrorInv); 25 | const FailureInfo = invert(FailureInfoInv); 26 | return {Error, FailureInfo, ErrorInv, FailureInfoInv}; 27 | } 28 | 29 | const carefulMathPath = path.join(__dirname, '..', 'contracts', 'CarefulMath.sol'); 30 | const CarefulMath = solparse.parseFile(carefulMathPath).body.find(k => k.type === 'ContractStatement'); 31 | const MathErrorInv = CarefulMath.body.find(k => k.name == 'MathError').members; 32 | const MathError = invert(MathErrorInv); 33 | 34 | const whitePaperModelPath = path.join(__dirname, '..', 'contracts', 'WhitePaperInterestRateModel.sol'); 35 | const whitePaperModel = solparse.parseFile(whitePaperModelPath).body.find(k => k.type === 'ContractStatement'); 36 | 37 | module.exports = { 38 | ComptrollerErr: parse(ComptrollerErrorReporter), 39 | TokenErr: parse(TokenErrorReporter), 40 | MathErr: { 41 | Error: MathError, 42 | ErrorInv: MathErrorInv 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /contracts/Maximillion.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./CEther.sol"; 4 | 5 | /** 6 | * @title Compound's Maximillion Contract 7 | * @author Compound 8 | */ 9 | contract Maximillion { 10 | /** 11 | * @notice The default cEther market to repay in 12 | */ 13 | CEther public cEther; 14 | 15 | /** 16 | * @notice Construct a Maximillion to repay max in a CEther market 17 | */ 18 | constructor(CEther cEther_) public { 19 | cEther = cEther_; 20 | } 21 | 22 | /** 23 | * @notice msg.sender sends Ether to repay an account's borrow in the cEther market 24 | * @dev The provided Ether is applied towards the borrow balance, any excess is refunded 25 | * @param borrower The address of the borrower account to repay on behalf of 26 | */ 27 | function repayBehalf(address borrower) public payable { 28 | repayBehalfExplicit(borrower, cEther); 29 | } 30 | 31 | /** 32 | * @notice msg.sender sends Ether to repay an account's borrow in a cEther market 33 | * @dev The provided Ether is applied towards the borrow balance, any excess is refunded 34 | * @param borrower The address of the borrower account to repay on behalf of 35 | * @param cEther_ The address of the cEther contract to repay in 36 | */ 37 | function repayBehalfExplicit(address borrower, CEther cEther_) public payable { 38 | uint received = msg.value; 39 | uint borrows = cEther_.borrowBalanceCurrent(borrower); 40 | if (received > borrows) { 41 | cEther_.repayBorrowBehalf.value(borrows)(borrower); 42 | msg.sender.transfer(received - borrows); 43 | } else { 44 | cEther_.repayBorrowBehalf.value(received)(borrower); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spec/certora/Governor/votes.cvl: -------------------------------------------------------------------------------- 1 | voteOnce(uint proposalId, bool support) { 2 | env e0; 3 | env e1; 4 | require e0.msg.sender == e1.msg.sender; 5 | 6 | uint nProposals = sinvoke proposalCount(e0); 7 | require proposalId < nProposals; 8 | 9 | invoke castVote(e0, proposalId, support); 10 | bool firstVoteReverted = lastReverted; 11 | 12 | invoke castVote(e1, proposalId, support); 13 | bool secondVoteReverted = lastReverted; 14 | 15 | assert !firstVoteReverted => secondVoteReverted, "Second vote succeeded after first"; 16 | } 17 | 18 | votesSum(uint proposalId, bool support, address voterA, address voterB) { 19 | env e0; 20 | env e1; 21 | require e0.msg.sender == voterA; 22 | require e1.msg.sender == voterB; 23 | 24 | uint nProposals = sinvoke proposalCount(e0); 25 | require proposalId < nProposals; 26 | 27 | uint preVotesFor = sinvoke certoraProposalVotesFor(e0, proposalId); 28 | uint preVotesAgainst = sinvoke certoraProposalVotesAgainst(e0, proposalId); 29 | 30 | sinvoke castVote(e0, proposalId, support); 31 | sinvoke castVote(e1, proposalId, support); 32 | 33 | uint postVotesFor = sinvoke certoraProposalVotesFor(e1, proposalId); 34 | uint postVotesAgainst = sinvoke certoraProposalVotesAgainst(e1, proposalId); 35 | 36 | uint voterAVotes = sinvoke certoraVoterVotes(e1, proposalId, voterA); 37 | uint voterBVotes = sinvoke certoraVoterVotes(e1, proposalId, voterB); 38 | 39 | // XXX violates? 40 | assert postVotesFor >= preVotesFor, "Cannot reduce votes for"; 41 | assert postVotesAgainst >= preVotesAgainst, "Cannot reduce votes against"; 42 | assert postVotesFor - preVotesFor + postVotesAgainst - preVotesAgainst == voterAVotes + voterBVotes, "Delta votes equals voter votes"; 43 | } 44 | -------------------------------------------------------------------------------- /spec/certora/contracts/UnderlyingModelNonStandard.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/EIP20NonStandardInterface.sol"; 4 | 5 | import "./SimulationInterface.sol"; 6 | 7 | contract UnderlyingModelNonStandard is EIP20NonStandardInterface, SimulationInterface { 8 | uint256 _totalSupply; 9 | mapping (address => uint256) balances; 10 | mapping (address => mapping (address => uint256)) allowances; 11 | 12 | function totalSupply() external view returns (uint256) { 13 | return _totalSupply; 14 | } 15 | 16 | function balanceOf(address owner) external view returns (uint256 balance) { 17 | balance = balances[owner]; 18 | } 19 | 20 | function transfer(address dst, uint256 amount) external { 21 | address src = msg.sender; 22 | require(balances[src] >= amount); 23 | require(balances[dst] + amount >= balances[dst]); 24 | 25 | balances[src] -= amount; 26 | balances[dst] += amount; 27 | } 28 | 29 | function transferFrom(address src, address dst, uint256 amount) external { 30 | require(allowances[src][msg.sender] >= amount); 31 | require(balances[src] >= amount); 32 | require(balances[dst] + amount >= balances[dst]); 33 | 34 | allowances[src][msg.sender] -= amount; 35 | balances[src] -= amount; 36 | balances[dst] += amount; 37 | } 38 | 39 | function approve(address spender, uint256 amount) external returns (bool success) { 40 | allowances[msg.sender][spender] = amount; 41 | } 42 | 43 | function allowance(address owner, address spender) external view returns (uint256 remaining) { 44 | remaining = allowances[owner][spender]; 45 | } 46 | 47 | function dummy() external { 48 | return; 49 | } 50 | } -------------------------------------------------------------------------------- /tests/Contracts/MockSushiBar.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "./ERC20.sol"; 4 | 5 | // SushiBar is the mock contract for testing. 6 | // Ref: https://etherscan.io/address/0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272#code 7 | contract SushiBar is StandardToken(0, "SushiBar", 18, "xSUSHI") { 8 | using SafeMath for uint256; 9 | ERC20 public sushi; 10 | 11 | constructor(ERC20 _sushi) public { 12 | sushi = _sushi; 13 | } 14 | 15 | function _mint(address _to, uint256 _amount) internal returns (bool) { 16 | totalSupply = totalSupply.add(_amount); 17 | balanceOf[_to] = balanceOf[_to].add(_amount); 18 | return true; 19 | } 20 | 21 | function _burn(address _to, uint256 _amount) internal returns (bool) { 22 | totalSupply = totalSupply.sub(_amount); 23 | balanceOf[_to] = balanceOf[_to].sub(_amount); 24 | return true; 25 | } 26 | 27 | // Enter the bar. Pay some SUSHIs. Earn some shares. 28 | function enter(uint256 _amount) public { 29 | uint256 totalSushi = sushi.balanceOf(address(this)); 30 | uint256 totalShares = totalSupply; 31 | if (totalShares == 0 || totalSushi == 0) { 32 | _mint(msg.sender, _amount); 33 | } else { 34 | uint256 what = _amount.mul(totalShares).div(totalSushi); 35 | _mint(msg.sender, what); 36 | } 37 | sushi.transferFrom(msg.sender, address(this), _amount); 38 | } 39 | 40 | // Leave the bar. Claim back your SUSHIs. 41 | function leave(uint256 _share) public { 42 | uint256 totalShares = totalSupply; 43 | uint256 what = _share.mul(sushi.balanceOf(address(this))).div(totalShares); 44 | _burn(msg.sender, _share); 45 | sushi.transfer(msg.sender, what); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Contracts/ComptrollerScenarioG3.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../contracts/ComptrollerG3.sol"; 4 | 5 | contract ComptrollerScenarioG3 is ComptrollerG3 { 6 | uint public blockNumber; 7 | address public compAddress; 8 | 9 | constructor() ComptrollerG3() public {} 10 | 11 | function setCompAddress(address compAddress_) public { 12 | compAddress = compAddress_; 13 | } 14 | 15 | function getCompAddress() public view returns (address) { 16 | return compAddress; 17 | } 18 | 19 | function membershipLength(CToken cToken) public view returns (uint) { 20 | return accountAssets[address(cToken)].length; 21 | } 22 | 23 | function fastForward(uint blocks) public returns (uint) { 24 | blockNumber += blocks; 25 | 26 | return blockNumber; 27 | } 28 | 29 | function setBlockNumber(uint number) public { 30 | blockNumber = number; 31 | } 32 | 33 | function getBlockNumber() public view returns (uint) { 34 | return blockNumber; 35 | } 36 | 37 | function getCompMarkets() public view returns (address[] memory) { 38 | uint m = allMarkets.length; 39 | uint n = 0; 40 | for (uint i = 0; i < m; i++) { 41 | if (markets[address(allMarkets[i])].isComped) { 42 | n++; 43 | } 44 | } 45 | 46 | address[] memory compMarkets = new address[](n); 47 | uint k = 0; 48 | for (uint i = 0; i < m; i++) { 49 | if (markets[address(allMarkets[i])].isComped) { 50 | compMarkets[k++] = address(allMarkets[i]); 51 | } 52 | } 53 | return compMarkets; 54 | } 55 | 56 | function unlist(CToken cToken) public { 57 | markets[address(cToken)].isListed = false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /script/saddle/verifyToken.js: -------------------------------------------------------------------------------- 1 | let { loadAddress, loadConf } = require('./support/tokenConfig'); 2 | 3 | function printUsage() { 4 | console.log(` 5 | usage: npx saddle script token:verify {tokenAddress} {tokenConfig} 6 | 7 | note: $ETHERSCAN_API_KEY environment variable must be set to an Etherscan API Key. 8 | 9 | example: 10 | 11 | npx saddle -n rinkeby script token:verify 0x19B674715cD20626415C738400FDd0d32D6809B6 '{ 12 | "underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A", 13 | "comptroller": "$Comptroller", 14 | "interestRateModel": "$Base200bps_Slope3000bps", 15 | "initialExchangeRateMantissa": "2.0e18", 16 | "name": "Compound Kyber Network Crystal", 17 | "symbol": "cKNC", 18 | "decimals": "8", 19 | "admin": "$Timelock" 20 | }' 21 | `); 22 | } 23 | 24 | (async function() { 25 | if (args.length !== 2) { 26 | return printUsage(); 27 | } 28 | 29 | let address = loadAddress(args[0], addresses); 30 | let conf = loadConf(args[1], addresses); 31 | if (!conf) { 32 | return printUsage(); 33 | } 34 | let etherscanApiKey = env['ETHERSCAN_API_KEY']; 35 | if (!etherscanApiKey) { 36 | console.error("Missing required $ETHERSCAN_API_KEY env variable."); 37 | return printUsage(); 38 | } 39 | 40 | console.log(`Verifying cToken at ${address} with ${JSON.stringify(conf)}`); 41 | 42 | let deployArgs = [conf.underlying, conf.comptroller, conf.interestRateModel, conf.initialExchangeRateMantissa.toString(), conf.name, conf.symbol, conf.decimals, conf.admin]; 43 | 44 | // TODO: Make sure we match optimizations count, etc 45 | await saddle.verify(etherscanApiKey, address, 'CErc20Immutable', deployArgs, 200, undefined); 46 | 47 | console.log(`Contract verified at https://${network}.etherscan.io/address/${address}`); 48 | 49 | return { 50 | ...conf, 51 | address 52 | }; 53 | })(); 54 | -------------------------------------------------------------------------------- /spec/scenario/CTokenAdmin.scen: -------------------------------------------------------------------------------- 1 | 2 | Test "Set admin" 3 | NewComptroller 4 | NewCToken ZRX cZRX 5 | Assert Equal (CToken cZRX Admin) (Address Root) 6 | Assert Equal (CToken cZRX PendingAdmin) (Address Zero) 7 | From Root (CToken cZRX SetPendingAdmin Geoff) 8 | Assert Equal (CToken cZRX Admin) (Address Root) 9 | Assert Equal (CToken cZRX PendingAdmin) (Address Geoff) 10 | From Geoff (CToken cZRX AcceptAdmin) 11 | Assert Equal (CToken cZRX Admin) (Address Geoff) 12 | Assert Equal (CToken cZRX PendingAdmin) (Address Zero) 13 | 14 | Test "Set admin to contructor argument" 15 | NewComptroller 16 | NewCToken ZRX cZRX admin:Torrey 17 | Assert Equal (CToken cZRX Admin) (Address Torrey) 18 | Assert Equal (CToken cZRX PendingAdmin) (Address Zero) 19 | From Torrey (CToken cZRX SetPendingAdmin Geoff) 20 | Assert Equal (CToken cZRX Admin) (Address Torrey) 21 | Assert Equal (CToken cZRX PendingAdmin) (Address Geoff) 22 | From Geoff (CToken cZRX AcceptAdmin) 23 | Assert Equal (CToken cZRX Admin) (Address Geoff) 24 | Assert Equal (CToken cZRX PendingAdmin) (Address Zero) 25 | 26 | 27 | Test "Fail to set pending admin" 28 | NewComptroller 29 | NewCToken ZRX cZRX 30 | Invariant Remains (CToken cZRX Admin) (Address Root) 31 | Invariant Remains (CToken cZRX PendingAdmin) (Address Zero) 32 | AllowFailures 33 | From Geoff (CToken cZRX SetPendingAdmin Geoff) 34 | Assert Failure UNAUTHORIZED SET_PENDING_ADMIN_OWNER_CHECK 35 | 36 | Test "Fail to accept admin" 37 | NewComptroller 38 | NewCToken ZRX cZRX 39 | Invariant Remains (CToken cZRX Admin) (Address Root) 40 | Invariant Remains (CToken cZRX PendingAdmin) (Address Zero) 41 | AllowFailures 42 | From Geoff (CToken cZRX AcceptAdmin) 43 | Assert Failure UNAUTHORIZED ACCEPT_ADMIN_PENDING_ADMIN_CHECK 44 | -------------------------------------------------------------------------------- /scenario/src/Value/CTokenDelegateValue.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../Event'; 2 | import { World } from '../World'; 3 | import { CErc20Delegate } from '../Contract/CErc20Delegate'; 4 | import { 5 | getCoreValue, 6 | mapValue 7 | } from '../CoreValue'; 8 | import { Arg, Fetcher, getFetcherValue } from '../Command'; 9 | import { 10 | AddressV, 11 | Value, 12 | } from '../Value'; 13 | import { getWorldContractByAddress, getCTokenDelegateAddress } from '../ContractLookup'; 14 | 15 | export async function getCTokenDelegateV(world: World, event: Event): Promise { 16 | const address = await mapValue( 17 | world, 18 | event, 19 | (str) => new AddressV(getCTokenDelegateAddress(world, str)), 20 | getCoreValue, 21 | AddressV 22 | ); 23 | 24 | return getWorldContractByAddress(world, address.val); 25 | } 26 | 27 | async function cTokenDelegateAddress(world: World, cTokenDelegate: CErc20Delegate): Promise { 28 | return new AddressV(cTokenDelegate._address); 29 | } 30 | 31 | export function cTokenDelegateFetchers() { 32 | return [ 33 | new Fetcher<{ cTokenDelegate: CErc20Delegate }, AddressV>(` 34 | #### Address 35 | 36 | * "CTokenDelegate Address" - Returns address of CTokenDelegate contract 37 | * E.g. "CTokenDelegate cDaiDelegate Address" - Returns cDaiDelegate's address 38 | `, 39 | "Address", 40 | [ 41 | new Arg("cTokenDelegate", getCTokenDelegateV) 42 | ], 43 | (world, { cTokenDelegate }) => cTokenDelegateAddress(world, cTokenDelegate), 44 | { namePos: 1 } 45 | ), 46 | ]; 47 | } 48 | 49 | export async function getCTokenDelegateValue(world: World, event: Event): Promise { 50 | return await getFetcherValue("CTokenDelegate", cTokenDelegateFetchers(), world, event); 51 | } 52 | -------------------------------------------------------------------------------- /scenario/src/Value/PriceOracleValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import {PriceOracle} from '../Contract/PriceOracle'; 4 | import { 5 | getAddressV 6 | } from '../CoreValue'; 7 | import { 8 | AddressV, 9 | NumberV, 10 | Value} from '../Value'; 11 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 12 | import {getPriceOracle} from '../ContractLookup'; 13 | 14 | async function getPrice(world: World, priceOracle: PriceOracle, asset: string): Promise { 15 | return new NumberV(await priceOracle.methods.assetPrices(asset).call()); 16 | } 17 | 18 | export async function getPriceOracleAddress(world: World, priceOracle: PriceOracle): Promise { 19 | return new AddressV(priceOracle._address); 20 | } 21 | 22 | export function priceOracleFetchers() { 23 | return [ 24 | new Fetcher<{priceOracle: PriceOracle}, AddressV>(` 25 | #### Address 26 | 27 | * "Address" - Gets the address of the global price oracle 28 | `, 29 | "Address", 30 | [ 31 | new Arg("priceOracle", getPriceOracle, {implicit: true}) 32 | ], 33 | (world, {priceOracle}) => getPriceOracleAddress(world, priceOracle) 34 | ), 35 | new Fetcher<{priceOracle: PriceOracle, asset: AddressV}, NumberV>(` 36 | #### Price 37 | 38 | * "Price asset:
" - Gets the price of the given asset 39 | `, 40 | "Price", 41 | [ 42 | new Arg("priceOracle", getPriceOracle, {implicit: true}), 43 | new Arg("asset", getAddressV,) 44 | ], 45 | (world, {priceOracle, asset}) => getPrice(world, priceOracle, asset.val) 46 | ) 47 | ]; 48 | } 49 | 50 | export async function getPriceOracleValue(world: World, event: Event): Promise { 51 | return await getFetcherValue("PriceOracle", priceOracleFetchers(), world, event); 52 | } 53 | -------------------------------------------------------------------------------- /script/scen/scriptFlywheel.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn run repl -s 2 | -- Deploys basic ComptrollerG3 3 | 4 | Unitroller Deploy 5 | PriceOracle Deploy Fixed 1.0 6 | PriceOracleProxy Deploy Admin (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) (Address Zero) (Address Zero) 7 | ----g2 8 | ComptrollerImpl Deploy ScenarioG2 ComptrollerScenG2 9 | Unitroller SetPendingImpl ComptrollerScenG2 10 | ComptrollerImpl ComptrollerScenG2 BecomeG2 11 | --list some tokens 12 | Comptroller SetPriceOracle (PriceOracleProxy Address) 13 | Comptroller SetMaxAssets 20 14 | Comptroller SetCloseFactor 0.5 15 | Comptroller LiquidationIncentive 1.1 16 | NewCToken ZRX cZRX 17 | NewCToken BAT cBAT 18 | Support cZRX collateralFactor:0.5 19 | Support cBAT collateralFactor:0.5 20 | -- final 21 | ComptrollerImpl Deploy Scenario ComptrollerScen 22 | Unitroller SetPendingImpl ComptrollerScen 23 | 24 | Prep Geoff 100e18 ZRX cZRX 25 | Mint Geoff 50e18 cZRX--tokenbalance = 50e18 / 2e9 = 2.5e10 26 | 27 | Prep Fourth Some BAT cBAT 28 | Mint Fourth 6e18 cBAT 29 | EnterMarkets Fourth cBAT 30 | Borrow Fourth 1e18 cZRX 31 | 32 | Prep Fifth Some BAT cBAT 33 | Mint Fifth 6e18 cBAT 34 | EnterMarkets Fifth cBAT 35 | Borrow Fifth 1e18 cZRX 36 | 37 | Prep Sixth Some BAT cBAT 38 | Mint Sixth 6e18 cBAT 39 | EnterMarkets Sixth cBAT 40 | Borrow Sixth 1e18 cZRX 41 | 42 | Prep Seventh Some BAT cBAT 43 | Mint Seventh 6e18 cBAT 44 | EnterMarkets Seventh cBAT 45 | Borrow Seventh 1e18 cZRX 46 | 47 | ComptrollerImpl ComptrollerScen Become 1e18 [cZRX cBAT] 48 | Erc20 Deploy Standard COMP "COMP Token" 18 49 | Give (Address Comptroller) 5000000e18 COMP 50 | Comptroller Send "setCompAddress(address)" (Address COMP) 51 | 52 | Comptroller RefreshCompSpeeds 53 | 54 | FastForward 300000 Blocks 55 | Read (Comptroller Address) 56 | Read (Address Fourth) 57 | Read (Address Fifth) 58 | Read (Address Sixth) 59 | Read (Address Seventh) 60 | -------------------------------------------------------------------------------- /scenario/src/Builder/UnitrollerBuilder.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {addAction, World} from '../World'; 3 | import {Unitroller} from '../Contract/Unitroller'; 4 | import {Invokation} from '../Invokation'; 5 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 6 | import {storeAndSaveContract} from '../Networks'; 7 | import {getContract} from '../Contract'; 8 | 9 | const UnitrollerContract = getContract("Unitroller"); 10 | 11 | export interface UnitrollerData { 12 | invokation: Invokation, 13 | description: string, 14 | address?: string 15 | } 16 | 17 | export async function buildUnitroller(world: World, from: string, event: Event): Promise<{world: World, unitroller: Unitroller, unitrollerData: UnitrollerData}> { 18 | const fetchers = [ 19 | new Fetcher<{}, UnitrollerData>(` 20 | #### Unitroller 21 | 22 | * "" - The Upgradable Comptroller 23 | * E.g. "Unitroller Deploy" 24 | `, 25 | "Unitroller", 26 | [], 27 | async (world, {}) => { 28 | return { 29 | invokation: await UnitrollerContract.deploy(world, from, []), 30 | description: "Unitroller" 31 | }; 32 | }, 33 | {catchall: true} 34 | ) 35 | ]; 36 | 37 | let unitrollerData = await getFetcherValue("DeployUnitroller", fetchers, world, event); 38 | let invokation = unitrollerData.invokation; 39 | delete unitrollerData.invokation; 40 | 41 | if (invokation.error) { 42 | throw invokation.error; 43 | } 44 | const unitroller = invokation.value!; 45 | unitrollerData.address = unitroller._address; 46 | 47 | world = await storeAndSaveContract( 48 | world, 49 | unitroller, 50 | 'Unitroller', 51 | invokation, 52 | [ 53 | { index: ['Unitroller'], data: unitrollerData } 54 | ] 55 | ); 56 | 57 | return {world, unitroller, unitrollerData}; 58 | } 59 | -------------------------------------------------------------------------------- /tests/CompilerTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | etherBalance, 3 | etherGasCost, 4 | getContract 5 | } = require('./Utils/Ethereum'); 6 | 7 | const { 8 | makeComptroller, 9 | makeCToken, 10 | makePriceOracle, 11 | pretendBorrow, 12 | borrowSnapshot 13 | } = require('./Utils/Compound'); 14 | 15 | describe('Const', () => { 16 | it("does the right thing and not too expensive", async () => { 17 | const base = await deploy('ConstBase'); 18 | const sub = await deploy('ConstSub'); 19 | expect(await call(base, 'c')).toEqual("1"); 20 | expect(await call(sub, 'c')).toEqual("2"); 21 | expect(await call(base, 'ADD', [1])).toEqual("2"); 22 | expect(await call(base, 'add', [1])).toEqual("2"); 23 | expect(await call(sub, 'ADD', [1])).toEqual("2"); 24 | expect(await call(sub, 'add', [1])).toEqual("3"); 25 | 26 | const tx1 = await send(base, 'ADD', [1]); 27 | const tx2 = await send(base, 'add', [1]); 28 | const tx3 = await send(sub, 'ADD', [1]); 29 | const tx4 = await send(sub, 'add', [1]); 30 | expect(Math.abs(tx2.gasUsed - tx1.gasUsed) < 20); 31 | expect(Math.abs(tx4.gasUsed - tx3.gasUsed) < 20); 32 | }); 33 | }); 34 | 35 | describe('Structs', () => { 36 | it("only writes one slot", async () => { 37 | const structs1 = await deploy('Structs'); 38 | const tx1_0 = await send(structs1, 'writeEach', [0, 1, 2, 3]); 39 | const tx1_1 = await send(structs1, 'writeEach', [0, 1, 2, 3]); 40 | const tx1_2 = await send(structs1, 'writeOnce', [0, 1, 2, 3]); 41 | 42 | const structs2 = await deploy('Structs'); 43 | const tx2_0 = await send(structs2, 'writeOnce', [0, 1, 2, 3]); 44 | const tx2_1 = await send(structs2, 'writeOnce', [0, 1, 2, 3]); 45 | const tx2_2 = await send(structs2, 'writeEach', [0, 1, 2, 3]); 46 | 47 | expect(tx1_0.gasUsed < tx2_0.gasUsed); // each beats once 48 | expect(tx1_1.gasUsed < tx2_1.gasUsed); // each beats once 49 | expect(tx1_2.gasUsed > tx2_2.gasUsed); // each beats once 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/Contracts/MockAggregator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | interface AggregatorV3Interface { 4 | function decimals() external view returns (uint8); 5 | function description() external view returns (string memory); 6 | function version() external view returns (uint256); 7 | 8 | // getRoundData and latestRoundData should both raise "No data present" 9 | // if they do not have data to report, instead of returning unset values 10 | // which could be misinterpreted as actual reported values. 11 | function getRoundData(uint80 _roundId) external view returns ( 12 | uint80 roundId, 13 | int256 answer, 14 | uint256 startedAt, 15 | uint256 updatedAt, 16 | uint80 answeredInRound 17 | ); 18 | 19 | function latestRoundData() external view returns ( 20 | uint80 roundId, 21 | int256 answer, 22 | uint256 startedAt, 23 | uint256 updatedAt, 24 | uint80 answeredInRound 25 | ); 26 | } 27 | 28 | contract MockAggregator { 29 | string public constant description = "mock aggregator"; 30 | uint256 public constant version = 1; 31 | uint80 public constant roundId = 1; 32 | 33 | uint8 public decimals = 18; 34 | int256 public answer; 35 | 36 | constructor(int256 _answer) public { 37 | answer = _answer; 38 | } 39 | 40 | function getRoundData(uint80 _roundId) external view returns (uint80, int256, uint256, uint256, uint80) { 41 | // Shh 42 | _roundId; 43 | 44 | return (roundId, answer, block.timestamp, block.timestamp, roundId); 45 | } 46 | 47 | function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { 48 | return (roundId, answer, block.timestamp, block.timestamp, roundId); 49 | } 50 | 51 | function setAnswer(int256 _answer) external { 52 | answer = _answer; 53 | } 54 | 55 | function setDecimals(uint8 _decimals) external { 56 | decimals = _decimals; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scenario/src/Contract/Governor.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '../Contract'; 2 | import { Callable, Sendable } from '../Invokation'; 3 | import { encodedNumber } from '../Encoding'; 4 | 5 | export interface Proposal { 6 | id: number 7 | proposer: string 8 | eta: number 9 | targets: string[] 10 | values: number[] 11 | signatures: string[] 12 | calldatas: string[] 13 | startBlock: number 14 | endBlock: number 15 | forVotes: number 16 | againstVotes: number 17 | } 18 | 19 | export const proposalStateEnums = { 20 | 0: "Pending", 21 | 1: "Active", 22 | 2: "Canceled", 23 | 3: "Defeated", 24 | 4: "Succeeded", 25 | 5: "Queued", 26 | 6: "Expired", 27 | 7: "Executed" 28 | } 29 | 30 | export interface GovernorMethods { 31 | guardian(): Callable; 32 | propose(targets: string[], values: encodedNumber[], signatures: string[], calldatas: string[], description: string): Sendable 33 | proposals(proposalId: number): Callable; 34 | proposalCount(): Callable; 35 | latestProposalIds(proposer: string): Callable; 36 | getReceipt(proposalId: number, voter: string): Callable<{ hasVoted: boolean, support: boolean, votes: number }>; 37 | castVote(proposalId: number, support: boolean): Sendable; 38 | queue(proposalId: encodedNumber): Sendable; 39 | execute(proposalId: encodedNumber): Sendable; 40 | cancel(proposalId: encodedNumber): Sendable; 41 | setBlockNumber(blockNumber: encodedNumber): Sendable; 42 | setBlockTimestamp(blockTimestamp: encodedNumber): Sendable; 43 | state(proposalId: encodedNumber): Callable; 44 | __queueSetTimelockPendingAdmin(newPendingAdmin: string, eta: encodedNumber): Sendable; 45 | __executeSetTimelockPendingAdmin(newPendingAdmin: string, eta: encodedNumber): Sendable; 46 | __acceptAdmin(): Sendable; 47 | __abdicate(): Sendable; 48 | } 49 | 50 | export interface Governor extends Contract { 51 | methods: GovernorMethods; 52 | name: string; 53 | } 54 | -------------------------------------------------------------------------------- /spec/certora/contracts/mcd/Lib.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity ^0.5.16; 15 | 16 | contract LibNote { 17 | event LogNote( 18 | bytes4 indexed sig, 19 | address indexed usr, 20 | bytes32 indexed arg1, 21 | bytes32 indexed arg2, 22 | bytes data 23 | ) anonymous; 24 | 25 | modifier note { 26 | _; 27 | assembly { 28 | // log an 'anonymous' event with a constant 6 words of calldata 29 | // and four indexed topics: selector, caller, arg1 and arg2 30 | let mark := msize // end of memory ensures zero 31 | mstore(0x40, add(mark, 288)) // update free memory pointer 32 | mstore(mark, 0x20) // bytes type data offset 33 | mstore(add(mark, 0x20), 224) // bytes size (padded) 34 | calldatacopy(add(mark, 0x40), 0, 224) // bytes payload 35 | log4(mark, 288, // calldata 36 | shl(224, shr(224, calldataload(0))), // msg.sig 37 | caller, // msg.sender 38 | calldataload(4), // arg1 39 | calldataload(36) // arg2 40 | ) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/certora/contracts/UnderlyingModelWithFee.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | import "../../../contracts/EIP20NonStandardInterface.sol"; 4 | 5 | import "./SimulationInterface.sol"; 6 | 7 | contract UnderlyingModelWithFee is EIP20NonStandardInterface, SimulationInterface { 8 | uint256 _totalSupply; 9 | uint256 fee; 10 | mapping (address => uint256) balances; 11 | mapping (address => mapping (address => uint256)) allowances; 12 | 13 | function totalSupply() external view returns (uint256) { 14 | return _totalSupply; 15 | } 16 | 17 | function balanceOf(address owner) external view returns (uint256 balance) { 18 | balance = balances[owner]; 19 | } 20 | 21 | function transfer(address dst, uint256 amount) external { 22 | address src = msg.sender; 23 | uint256 actualAmount = amount + fee; 24 | require(actualAmount >= amount); 25 | require(balances[src] >= actualAmount); 26 | require(balances[dst] + actualAmount >= balances[dst]); 27 | 28 | balances[src] -= actualAmount; 29 | balances[dst] += actualAmount; 30 | } 31 | 32 | function transferFrom(address src, address dst, uint256 amount) external { 33 | uint256 actualAmount = amount + fee; 34 | require(actualAmount > fee) 35 | require(allowances[src][msg.sender] >= actualAmount); 36 | require(balances[src] >= actualAmount); 37 | require(balances[dst] + actualAmount >= balances[dst]); 38 | 39 | allowances[src][msg.sender] -= actualAmount; 40 | balances[src] -= actualAmount; 41 | balances[dst] += actualAmount; 42 | } 43 | 44 | function approve(address spender, uint256 amount) external returns (bool success) { 45 | allowances[msg.sender][spender] = amount; 46 | } 47 | 48 | function allowance(address owner, address spender) external view returns (uint256 remaining) { 49 | remaining = allowances[owner][spender]; 50 | } 51 | 52 | function dummy() external { 53 | return; 54 | } 55 | } -------------------------------------------------------------------------------- /script/scen/deploy.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn run repl -s 2 | -- Deploys new Comptroller with some ERC20 and some cTokens 3 | 4 | -- First deploy a price oracle 5 | Gate (PriceOracle Address) (PriceOracle Deploy Simple) 6 | 7 | -- Next a comptroller 8 | Gate (Comptroller Address) (Comptroller Deploy YesNo) 9 | 10 | -- Next an interest rate model 11 | Gate (InterestRateModel InterestRateModel Address) (InterestRateModel Deploy Fixed InterestRateModel 0.0004) 12 | 13 | -- Now deploy some ERC-20 faucet tokens 14 | Gate (Erc20 ZRX Address) (Erc20 Deploy Standard ZRX "0x") 15 | Gate (Erc20 BAT Address) (Erc20 Deploy NonStandard BAT "Basic Attention Token") 16 | Gate (Erc20 DAI Address) (Erc20 Deploy Standard DAI "Dai") 17 | Gate (Erc20 REP Address) (Erc20 Deploy Standard REP "Augur") 18 | Gate (Erc20 USDC Address) (Erc20 Deploy Standard USDC "USD Coin" 6) 19 | 20 | -- Now deploy our cTokens 21 | Gate (CToken cZRX Address) (CToken Deploy CErc20 cZRX "Test 0x 📈" (Erc20 ZRX Address) (Comptroller Address) (InterestRateModel InterestRateModel Address) 0.2e9 8) 22 | Gate (CToken cBAT Address) (CToken Deploy CErc20 cBAT "Test Basic Attention Token 📈" (Erc20 BAT Address) (Comptroller Address) (InterestRateModel InterestRateModel Address) 0.2e9 8) 23 | Gate (CToken cDAI Address) (CToken Deploy CErc20 cDAI "Test Dai 📈" (Erc20 DAI Address) (Comptroller Address) (InterestRateModel InterestRateModel Address) 0.2e9 8) 24 | Gate (CToken cREP Address) (CToken Deploy CErc20 cREP "Test Augur 📈" (Erc20 REP Address) (Comptroller Address) (InterestRateModel InterestRateModel Address) 0.2e9 8) 25 | Gate (CToken cETH Address) (CToken Deploy CEther cETH "Test Ether 📈" (Comptroller Address) (InterestRateModel InterestRateModel Address) 0.2e9 8) 26 | Gate (CToken cUSDC Address) (CToken Deploy CErc20 cUSDC "Test USD Coin 📈" (Erc20 USDC Address) (Comptroller Address) (InterestRateModel InterestRateModel Address) 2e-4 8) 27 | 28 | -- Deploy Maximillion 29 | Gate (Maximillion Address) (Maximillion Deploy cETH) 30 | 31 | Print "Deployed Comptroller and cTokens: cETH, cBAT, cDAI, cREP, cUSDC and cZRX" 32 | -------------------------------------------------------------------------------- /scenario/src/Event/TrxEvent.ts: -------------------------------------------------------------------------------- 1 | import {World} from '../World'; 2 | import {Event} from '../Event'; 3 | import {processCoreEvent} from '../CoreEvent'; 4 | import { 5 | EventV, 6 | NumberV 7 | } from '../Value'; 8 | import { 9 | getEventV, 10 | getNumberV 11 | } from '../CoreValue'; 12 | import {Arg, Command, processCommandEvent} from '../Command'; 13 | import {encodedNumber} from '../Encoding'; 14 | 15 | async function setTrxValue(world: World, value: encodedNumber): Promise { 16 | return world.update('trxInvokationOpts', (t) => t.set('value', value.toString())); 17 | } 18 | 19 | async function setTrxGasPrice(world: World, gasPrice: encodedNumber): Promise { 20 | return world.update('trxInvokationOpts', (t) => t.set('gasPrice', gasPrice.toString()));; 21 | } 22 | 23 | export function trxCommands() { 24 | return [ 25 | new Command<{amount: NumberV, event: EventV}>(` 26 | #### Value 27 | 28 | * "Value " - Runs event with a set amount for any transactions 29 | * E.g. "Value 1.0e18 (CToken cEth Mint 1.0e18)" 30 | `, 31 | "Value", 32 | [ 33 | new Arg("amount", getNumberV), 34 | new Arg("event", getEventV) 35 | ], 36 | async (world, from, {amount, event}) => processCoreEvent(await setTrxValue(world, amount.encode()), event.val, from) 37 | ), 38 | new Command<{gasPrice: NumberV, event: EventV}>(` 39 | #### GasPrice 40 | 41 | * "GasPrice " - Runs event with a given gas price 42 | * E.g. "GasPrice 0 (CToken cEth Mint 1.0e18)" 43 | `, 44 | "GasPrice", 45 | [ 46 | new Arg("gasPrice", getNumberV), 47 | new Arg("event", getEventV) 48 | ], 49 | async (world, from, {gasPrice, event}) => processCoreEvent(await setTrxGasPrice(world, gasPrice.encode()), event.val, from) 50 | ) 51 | ]; 52 | } 53 | 54 | export async function processTrxEvent(world: World, event: Event, from: string | null): Promise { 55 | return await processCommandEvent("Trx", trxCommands(), world, event, from); 56 | } 57 | -------------------------------------------------------------------------------- /scenario/src/Contract/builder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let [_, _f, buildFile, contract] = process.argv; 5 | 6 | if (!buildFile || !contract) { 7 | throw new Error(`builder.js `); 8 | } 9 | if (!fs.existsSync(buildFile)) { 10 | throw new Error(`build_file: file not found`); 11 | } 12 | let buildRaw = fs.readFileSync(buildFile, 'utf8'); 13 | let build; 14 | 15 | try { 16 | build = JSON.parse(buildRaw); 17 | } catch (e) { 18 | throw new Error(`Error parsing build file: ${e.toString()}`); 19 | } 20 | if (!build.contracts) { 21 | throw new Error(`Invalid build file, missing contracts`); 22 | } 23 | let contractInfo = Object.entries(build.contracts).find(([k,v]) => k.split(':')[1] === contract); 24 | if (!contractInfo) { 25 | throw new Error(`Build file does not contain info for ${contract}`); 26 | } 27 | let contractABI = JSON.parse(contractInfo[1].abi); 28 | 29 | console.log(`export interface ${contract}Methods {`); 30 | contractABI.forEach(abi => { 31 | if (abi.type === 'function') { 32 | function mapped(io) { 33 | let typeMap = { 34 | 'address': 'string', 35 | 'address[]': 'string[]', 36 | 'uint256': 'number', 37 | 'bool': 'boolean' 38 | }; 39 | return typeMap[io.type] || io.type; 40 | }; 41 | let name = abi.name; 42 | let args = abi.inputs.map((input) => { 43 | return `${input.name}: ${mapped(input)}`; 44 | }).join(', '); 45 | let returnType = abi.outputs.map((output) => { 46 | if (output.type == 'tuple' || output.type == 'tuple[]') { 47 | let res = output.components.map((c) => { 48 | return mapped(c); 49 | }).join(','); 50 | if (output.type == 'tuple[]') { 51 | return `[${res}][]`; 52 | } else { 53 | return `[${res}]`; 54 | } 55 | } else { 56 | return mapped(output); 57 | } 58 | }).join(','); 59 | let able = abi.constant ? 'Callable' : 'Sendable'; 60 | console.log(` ${name}(${args}): ${able}<${returnType}>;`); 61 | } 62 | }); 63 | console.log("}"); 64 | -------------------------------------------------------------------------------- /spec/scenario/BorrowEth.scen: -------------------------------------------------------------------------------- 1 | 2 | Test "Borrow some Eth enters Eth and succeeds when Eth not entered" 3 | NewComptroller price:1.0 4 | ListedCToken ZRX cZRX 5 | ListedEtherToken cETH initialExchangeRate:0.005e9 6 | SetCollateralFactor cZRX collateralFactor:0.5 7 | SetCollateralFactor cETH collateralFactor:0.5 8 | Donate cETH 0.003e18 9 | Prep Geoff Some ZRX cZRX 10 | Mint Geoff 1e18 cZRX 11 | EnterMarkets Geoff cZRX 12 | Expect Changes (EtherBalance Geoff) +0.001e18 13 | BorrowEth Geoff 0.001e18 cETH 14 | Assert Equal (EtherBalance cETH) 0.002e18 15 | Assert Equal (Comptroller Liquidity Geoff) 4.99e17 16 | Assert Equal (Comptroller MembershipLength Geoff) (Exactly 2) 17 | Assert True (Comptroller CheckMembership Geoff cETH) 18 | 19 | Test "Borrow some ETH fails when no ETH available" 20 | NewComptroller price:1.0 21 | ListedCToken ZRX cZRX 22 | ListedEtherToken cETH initialExchangeRate:0.005e9 23 | SetCollateralFactor cZRX collateralFactor:0.5 24 | SetCollateralFactor cETH collateralFactor:0.5 25 | Prep Geoff Some ZRX cZRX 26 | Mint Geoff 100e18 cZRX 27 | EnterMarkets Geoff cZRX cETH 28 | AllowFailures 29 | Invariant Static (CToken cZRX ExchangeRateStored) 30 | Invariant Static (CToken cETH ExchangeRateStored) 31 | Invariant Static (Comptroller Liquidity Geoff) 32 | Invariant Static (EtherBalance Geoff) 33 | BorrowEth Geoff 1e18 cETH 34 | Assert Failure TOKEN_INSUFFICIENT_CASH BORROW_CASH_NOT_AVAILABLE 35 | 36 | Test "Borrow some ETH from excess cash" 37 | NewComptroller price:1.0 38 | ListedCToken ZRX cZRX 39 | ListedEtherToken cETH initialExchangeRate:0.005e9 40 | SetCollateralFactor cZRX collateralFactor:0.5 41 | SetCollateralFactor cETH collateralFactor:0.5 42 | Donate cETH 0.003e18 43 | Prep Geoff Some ZRX cZRX 44 | Mint Geoff 1e18 cZRX 45 | EnterMarkets Geoff cZRX cETH 46 | Expect Changes (EtherBalance Geoff) +0.001e18 47 | BorrowEth Geoff 0.001e18 cETH 48 | Assert Equal (EtherBalance cETH) 0.002e18 49 | Assert Equal (Comptroller Liquidity Geoff) 4.99e17 50 | -------------------------------------------------------------------------------- /tests/Tokens/safeTokenTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | makeCToken, 3 | getBalances, 4 | adjustBalances 5 | } = require('../Utils/Compound'); 6 | 7 | const exchangeRate = 5; 8 | 9 | describe('CEther', function () { 10 | let root, nonRoot, accounts; 11 | let cToken; 12 | beforeEach(async () => { 13 | [root, nonRoot, ...accounts] = saddle.accounts; 14 | cToken = await makeCToken({kind: 'cether', comptrollerOpts: {kind: 'bool'}}); 15 | }); 16 | 17 | describe("getCashPrior", () => { 18 | it("returns the amount of ether held by the cEther contract before the current message", async () => { 19 | expect(await call(cToken, 'harnessGetCashPrior', [], {value: 100})).toEqualNumber(0); 20 | }); 21 | }); 22 | 23 | describe("doTransferIn", () => { 24 | it("succeeds if from is msg.nonRoot and amount is msg.value", async () => { 25 | expect(await call(cToken, 'harnessDoTransferIn', [root, 100], {value: 100})).toEqualNumber(100); 26 | }); 27 | 28 | it("reverts if from != msg.sender", async () => { 29 | await expect(call(cToken, 'harnessDoTransferIn', [nonRoot, 100], {value: 100})).rejects.toRevert("revert sender mismatch"); 30 | }); 31 | 32 | it("reverts if amount != msg.value", async () => { 33 | await expect(call(cToken, 'harnessDoTransferIn', [root, 77], {value: 100})).rejects.toRevert("revert value mismatch"); 34 | }); 35 | 36 | describe("doTransferOut", () => { 37 | it("transfers ether out", async () => { 38 | const beforeBalances = await getBalances([cToken], [nonRoot]); 39 | const receipt = await send(cToken, 'harnessDoTransferOut', [nonRoot, 77], {value: 77}); 40 | const afterBalances = await getBalances([cToken], [nonRoot]); 41 | expect(receipt).toSucceed(); 42 | expect(afterBalances).toEqual(await adjustBalances(beforeBalances, [ 43 | [cToken, nonRoot, 'eth', 77] 44 | ])); 45 | }); 46 | 47 | it("reverts if it fails", async () => { 48 | await expect(call(cToken, 'harnessDoTransferOut', [root, 77], {value: 0})).rejects.toRevert(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/Flywheel/GasTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | makeComptroller, 3 | makeCToken 4 | } = require('../Utils/Compound'); 5 | const { 6 | etherExp, 7 | etherDouble, 8 | etherUnsigned 9 | } = require('../Utils/Ethereum'); 10 | 11 | 12 | // NB: coverage doesn't like this 13 | describe.skip('Flywheel trace ops', () => { 14 | let root, a1, a2, a3, accounts; 15 | let comptroller, market; 16 | beforeEach(async () => { 17 | let interestRateModelOpts = {borrowRate: 0.000001}; 18 | [root, a1, a2, a3, ...accounts] = saddle.accounts; 19 | comptroller = await makeComptroller(); 20 | market = await makeCToken({comptroller, supportMarket: true, underlyingPrice: 3, interestRateModelOpts}); 21 | await send(comptroller, '_addCompMarkets', [[market].map(c => c._address)]); 22 | }); 23 | 24 | it('update supply index SSTOREs', async () => { 25 | await send(comptroller, 'setBlockNumber', [100]); 26 | await send(market, 'harnessSetTotalBorrows', [etherUnsigned(11e18)]); 27 | await send(comptroller, 'setCompSpeed', [market._address, etherExp(0.5)]); 28 | 29 | const tx = await send(comptroller, 'harnessUpdateCompSupplyIndex', [market._address]); 30 | 31 | const ops = {}; 32 | await saddle.trace(tx, { 33 | execLog: log => { 34 | if (log.lastLog != undefined) { 35 | ops[log.op] = (ops[log.op] || []).concat(log); 36 | } 37 | } 38 | }); 39 | expect(ops.SSTORE.length).toEqual(1); 40 | }); 41 | 42 | it('update borrow index SSTOREs', async () => { 43 | await send(comptroller, 'setBlockNumber', [100]); 44 | await send(market, 'harnessSetTotalBorrows', [etherUnsigned(11e18)]); 45 | await send(comptroller, 'setCompSpeed', [market._address, etherExp(0.5)]); 46 | 47 | const tx = await send(comptroller, 'harnessUpdateCompBorrowIndex', [market._address, etherExp(1.1)]); 48 | 49 | const ops = {}; 50 | await saddle.trace(tx, { 51 | execLog: log => { 52 | if (log.lastLog != undefined) { 53 | ops[log.op] = (ops[log.op] || []).concat(log); 54 | } 55 | } 56 | }); 57 | expect(ops.SSTORE.length).toEqual(1); 58 | }); 59 | }); -------------------------------------------------------------------------------- /gasCosts.json: -------------------------------------------------------------------------------- 1 | { 2 | "second mint": { 3 | "fee": 86450, 4 | "opcodes": { 5 | "PUSH1 @ 3": 589, 6 | "MSTORE @ 12": 9, 7 | "CALLDATASIZE @ 2": 19, 8 | "LT @ 3": 34, 9 | "PUSH2 @ 3": 322, 10 | "JUMPI @ 10": 182, 11 | "CALLDATALOAD @ 3": 24, 12 | "SHR @ 3": 9, 13 | "DUP1 @ 3": 196, 14 | "PUSH4 @ 3": 73, 15 | "GT @ 3": 46, 16 | "JUMPDEST @ 1": 273, 17 | "EQ @ 3": 48, 18 | "CALLVALUE @ 2": 9, 19 | "ISZERO @ 3": 91, 20 | "POP @ 2": 308, 21 | "SUB @ 3": 62, 22 | "DUP2 @ 3": 183, 23 | "JUMP @ 8": 128, 24 | "SLOAD @ 800": 38, 25 | "MLOAD @ 3": 93, 26 | "SWAP2 @ 3": 108, 27 | "SHL @ 3": 35, 28 | "SWAP1 @ 3": 231, 29 | "AND @ 3": 51, 30 | "DUP3 @ 3": 77, 31 | "DUP4 @ 3": 75, 32 | "DUP5 @ 3": 22, 33 | "CALLDATACOPY @ 18": 1, 34 | "SWAP3 @ 3": 77, 35 | "ADD @ 3": 130, 36 | "SWAP5 @ 3": 10, 37 | "SWAP4 @ 3": 35, 38 | "DUP6 @ 3": 24, 39 | "GAS @ 2": 8, 40 | "DELEGATECALL @ 700": 1, 41 | "NOT @ 3": 4, 42 | "SSTORE @ 5000": 7, 43 | "MSTORE @ 3": 91, 44 | "MSTORE @ 9": 8, 45 | "MSTORE @ 6": 30, 46 | "MSTORE @ 24": 1, 47 | "ADDRESS @ 2": 7, 48 | "DUP7 @ 3": 19, 49 | "EXTCODESIZE @ 700": 7, 50 | "STATICCALL @ 700": 5, 51 | "SHA3 @ 42": 14, 52 | "RETURN @ 0": 9, 53 | "RETURNDATASIZE @ 2": 13, 54 | "DUP8 @ 3": 12, 55 | "SWAP6 @ 3": 7, 56 | "SWAP7 @ 3": 2, 57 | "PUSH8 @ 3": 8, 58 | "PUSH32 @ 3": 5, 59 | "DIV @ 5": 11, 60 | "PUSH6 @ 3": 1, 61 | "DUP9 @ 3": 11, 62 | "CODECOPY @ 12": 1, 63 | "CODECOPY @ 16": 2, 64 | "CODECOPY @ 15": 4, 65 | "DUP13 @ 3": 1, 66 | "DUP12 @ 3": 1, 67 | "DUP15 @ 3": 1, 68 | "SSTORE @ 800": 4, 69 | "DUP14 @ 3": 1, 70 | "MSTORE @ 7": 4, 71 | "LOG1 @ 1774": 1, 72 | "SWAP15 @ 3": 1, 73 | "CALLER @ 2": 3, 74 | "CALL @ 700": 2, 75 | "MUL @ 5": 3, 76 | "SWAP9 @ 3": 2, 77 | "SWAP8 @ 3": 2, 78 | "LOG3 @ 1756": 2, 79 | "RETURNDATACOPY @ 6": 2, 80 | "LOG1 @ 1518": 1, 81 | "CODECOPY @ 6": 1, 82 | "OR @ 3": 1, 83 | "RETURNDATACOPY @ 9": 1 84 | } 85 | }, 86 | "redeem": 94077, 87 | "first mint": 134954, 88 | "second mint, no interest accrued": 70851 89 | } -------------------------------------------------------------------------------- /spec/scenario/Withdraw.scen.old: -------------------------------------------------------------------------------- 1 | -- Withdraw Tests 2 | 3 | Test "Supply Ether 5 then Withdraw MAX in the same block" 4 | AddToken Ether -- baseline sanity check for withdraw max 5 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 6 | Approve Geoff Ether "6.0e18" 7 | Faucet Geoff Ether "6.0e18" 8 | Supply Geoff Ether "5.0e18" 9 | Assert Success 10 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "5.0e18") 11 | Assert Equal (BorrowBalance Geoff Ether) (Exactly "0e18") 12 | Assert Equal (MaxBorrow Geoff) (Exactly "2.5e18") 13 | Withdraw Geoff Ether "MAX" 14 | Assert Success 15 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "0.0e18") 16 | Assert Equal (TokenBalance Geoff Ether) (Exactly "6e18") 17 | 18 | Test "Supply Ether 5 then Withdraw MAX (6) after accruing some interest" 19 | AddToken Ether 20 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 21 | Approve Geoff Ether "6.0e18" 22 | Faucet Geoff Ether "6.0e18" 23 | Supply Geoff Ether "5.0e18" -- We need more Ether in the system to simulate protocol gaining borrow interest to pay Geoff 24 | Approve Torrey Ether "10.0e18" 25 | Faucet Torrey Ether "10.0e18" 26 | Supply Torrey Ether "10.0e18" 27 | Assert Success 28 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "5.0e18") 29 | FastForward 2 Blocks 30 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "10.0e18") 31 | Withdraw Geoff Ether "MAX" 32 | Assert Success 33 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "0.0e18") 34 | Assert Equal (TokenBalance Geoff Ether) (Exactly "11e18") 35 | 36 | Test "Withdraw Ether 1 when contract paused" 37 | AddToken Ether 38 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 39 | Approve Geoff Ether "1.0e18" 40 | Faucet Geoff Ether "1.0e18" 41 | Supply Geoff Ether "1.0e18" 42 | Assert Success 43 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "1.0e18") 44 | PolicyHook Ether (SetPaused True) 45 | Withdraw Geoff Ether "1.0e18" 46 | Assert Failure COMPTROLLER_REJECTION WITHDRAW_COMPTROLLER_REJECTION 1 47 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "1e18") 48 | Assert Equal (TokenBalance Geoff Ether) (Exactly "0e18") 49 | -------------------------------------------------------------------------------- /scenario/src/Builder/MaximillionBuilder.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {addAction, World} from '../World'; 3 | import {Maximillion} from '../Contract/Maximillion'; 4 | import {Invokation} from '../Invokation'; 5 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 6 | import {storeAndSaveContract} from '../Networks'; 7 | import {getContract} from '../Contract'; 8 | import {getAddressV} from '../CoreValue'; 9 | import {AddressV} from '../Value'; 10 | 11 | const MaximillionContract = getContract("Maximillion"); 12 | 13 | export interface MaximillionData { 14 | invokation: Invokation, 15 | description: string, 16 | cEtherAddress: string, 17 | address?: string 18 | } 19 | 20 | export async function buildMaximillion(world: World, from: string, event: Event): Promise<{world: World, maximillion: Maximillion, maximillionData: MaximillionData}> { 21 | const fetchers = [ 22 | new Fetcher<{cEther: AddressV}, MaximillionData>(` 23 | #### Maximillion 24 | 25 | * "" - Maximum Eth Repays Contract 26 | * E.g. "Maximillion Deploy" 27 | `, 28 | "Maximillion", 29 | [ 30 | new Arg("cEther", getAddressV) 31 | ], 32 | async (world, {cEther}) => { 33 | return { 34 | invokation: await MaximillionContract.deploy(world, from, [cEther.val]), 35 | description: "Maximillion", 36 | cEtherAddress: cEther.val 37 | }; 38 | }, 39 | {catchall: true} 40 | ) 41 | ]; 42 | 43 | let maximillionData = await getFetcherValue("DeployMaximillion", fetchers, world, event); 44 | let invokation = maximillionData.invokation; 45 | delete maximillionData.invokation; 46 | 47 | if (invokation.error) { 48 | throw invokation.error; 49 | } 50 | const maximillion = invokation.value!; 51 | maximillionData.address = maximillion._address; 52 | 53 | world = await storeAndSaveContract( 54 | world, 55 | maximillion, 56 | 'Maximillion', 57 | invokation, 58 | [ 59 | { index: ['Maximillion'], data: maximillionData } 60 | ] 61 | ); 62 | 63 | return {world, maximillion, maximillionData}; 64 | } 65 | -------------------------------------------------------------------------------- /spec/sim/0003-borrow-caps-patch/hypothetical_upgrade_post_deploy.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn repl -s 2 | 3 | PrintTransactionLogs 4 | Alias CompHolder "0x19bc62ff7cd9ffd6bdced9802ff718f09f7259f1" 5 | Alias USDCWhale "0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3" 6 | Alias Arr00 "0x2b384212edc04ae8bb41738d05ba20e33277bf33" 7 | Web3Fork "https://mainnet-eth.compound.finance/@10809638" (CompHolder USDCWhale) 8 | UseConfigs mainnet 9 | 10 | 11 | -- Propose to apply the patch 12 | 13 | From CompHolder (Comp Delegate CompHolder) 14 | From CompHolder (Governor GovernorAlpha Propose "Borrow Limit Comptroller Patch" [(Address Unitroller) (Address StdComptrollerG5) (Address Unitroller) (Address cSAI) (Address SAI)] [0 0 0 0 0] ["_setPendingImplementation(address)" "_become(address)" "_setBorrowCapGuardian(address)" "_reduceReserves(uint256)" "transfer(address,uint256)"] [[(Address StdComptrollerG5)] [(Address Unitroller)] [(Address CompHolder)] [2360000000000000000000] [(Address Arr00) 2360000000000000000000]]) 15 | 16 | -- Vote for, queue, and execute the proposal 17 | 18 | MineBlock 19 | From CompHolder (Governor GovernorAlpha Proposal LastProposal Vote For) 20 | AdvanceBlocks 20000 21 | Governor GovernorAlpha Proposal LastProposal Queue 22 | IncreaseTime 604910 23 | Governor GovernorAlpha Proposal LastProposal Execute 24 | ComptrollerImpl StdComptrollerG5 MergeABI 25 | 26 | Assert Equal (Address (Unitroller Implementation)) (Address StdComptrollerG5) 27 | Assert Equal (Erc20 SAI TokenBalance (Address Arr00)) (2360000000000000000000) 28 | 29 | From USDCWhale (Trx GasPrice 0 (Erc20 USDC Approve cUSDC UInt256Max)) 30 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Mint 100000000e6)) 31 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 1000000e6)) 32 | 33 | -- Market borrows were just under 81M at this block 34 | From CompHolder (Comptroller SetMarketBorrowCaps (cUSDC) (68000000e6)) 35 | 36 | AllowFailures 37 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 1000000e6)) 38 | Assert Revert 39 | 40 | Successfully 41 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC RepayBorrow 1000000e6)) 42 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 10e6)) 43 | 44 | 45 | 46 | Print "Borrow Limit Comptroller Patch OK!" -------------------------------------------------------------------------------- /spec/sim/0003-borrow-caps-patch/hypothetical_upgrade.scen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env yarn repl -s 2 | 3 | PrintTransactionLogs 4 | Alias CompHolder "0x19bc62ff7cd9ffd6bdced9802ff718f09f7259f1" 5 | Alias USDCWhale "0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3" 6 | Alias Arr00 "0x2b384212edc04ae8bb41738d05ba20e33277bf33" 7 | Web3Fork "https://mainnet-eth.compound.finance/@10706095" (CompHolder USDCWhale) 8 | UseConfigs mainnet 9 | 10 | -- Deploy the flywheel impl 11 | 12 | ComptrollerImpl Deploy Standard ComptrollerG5 13 | 14 | -- Propose to apply the patch 15 | 16 | From CompHolder (Comp Delegate CompHolder) 17 | From CompHolder (Governor GovernorAlpha Propose "Borrow Cap Comptroller Patch" [(Address Unitroller) (Address ComptrollerG5) (Address Unitroller) (Address cSAI) (Address SAI)] [0 0 0 0 0] ["_setPendingImplementation(address)" "_become(address)" "_setBorrowCapGuardian(address)" "_reduceReserves(uint256)" "transfer(address,uint256)"] [[(Address ComptrollerG5)] [(Address Unitroller)] [(Address CompHolder)] [2360000000000000000000] [(Address Arr00) 2360000000000000000000]]) 18 | 19 | -- Vote for, queue, and execute the proposal 20 | 21 | MineBlock 22 | From CompHolder (Governor GovernorAlpha Proposal LastProposal Vote For) 23 | AdvanceBlocks 20000 24 | Governor GovernorAlpha Proposal LastProposal Queue 25 | IncreaseTime 604910 26 | Governor GovernorAlpha Proposal LastProposal Execute 27 | ComptrollerImpl ComptrollerG5 MergeABI 28 | 29 | Assert Equal (Address (Unitroller Implementation)) (Address ComptrollerG5) 30 | Assert Equal (Erc20 SAI TokenBalance (Address Arr00)) (2360000000000000000000) 31 | 32 | From USDCWhale (Trx GasPrice 0 (Erc20 USDC Approve cUSDC UInt256Max)) 33 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Mint 214000000e6)) 34 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 1000000e6)) 35 | 36 | From CompHolder (Comptroller SetMarketBorrowCaps (cUSDC) (83000000e6)) 37 | 38 | AllowFailures 39 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 1000000e6)) 40 | Assert Revert 41 | 42 | Successfully 43 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC RepayBorrow 1000000e6)) 44 | From USDCWhale (Trx GasPrice 0 (CToken cUSDC Borrow 10e6)) 45 | 46 | 47 | 48 | Print "Borrow Cap Comptroller Patch OK!" -------------------------------------------------------------------------------- /scenario/src/Expectation/ChangesExpectation.ts: -------------------------------------------------------------------------------- 1 | import {Expectation} from '../Expectation'; 2 | import {fail, World} from '../World'; 3 | import {getCoreValue} from '../CoreValue'; 4 | import {Value, NumberV} from '../Value'; 5 | import {Event} from '../Event'; 6 | import {formatEvent} from '../Formatter'; 7 | import {BigNumber} from 'bignumber.js'; 8 | 9 | function asNumberV(v: Value): NumberV { 10 | if (v instanceof NumberV) { 11 | return v; 12 | } else { 13 | throw new Error(`Expected NumberV for ChangesExpectation, got ${v.toString()}`); 14 | } 15 | } 16 | 17 | export class ChangesExpectation implements Expectation { 18 | condition: Event; 19 | originalValue: NumberV; 20 | delta: NumberV; 21 | tolerance: NumberV; 22 | expected: NumberV; 23 | 24 | constructor(condition: Event, originalValue: Value, delta: NumberV, tolerance: NumberV) { 25 | this.condition = condition; 26 | this.originalValue = asNumberV(originalValue); 27 | this.delta = delta; 28 | this.tolerance = tolerance; 29 | this.expected = this.originalValue.add(this.delta); 30 | } 31 | 32 | async getCurrentValue(world: World): Promise { 33 | return await getCoreValue(world, this.condition); 34 | }; 35 | 36 | async checker(world: World, initialCheck: boolean=false): Promise { 37 | const currentValue = asNumberV(await this.getCurrentValue(world)); 38 | const trueDelta = currentValue.sub(this.originalValue); 39 | 40 | if (this.tolerance.val != 0) { 41 | if (Math.abs(Number(trueDelta.sub(this.delta).div(this.originalValue).val)) > Number(this.tolerance.val)) { 42 | fail(world, `Expected ${trueDelta.toString()} to approximately equal ${this.delta.toString()} within ${this.tolerance.toString()}`); 43 | } 44 | } else if (!currentValue.compareTo(world, this.expected)) { 45 | fail(world, `${this.toString()} instead had value \`${currentValue.toString()}\` (true delta: ${trueDelta.toString()})`); 46 | } 47 | } 48 | 49 | toString() { 50 | return `ChangesExpectation: condition=${formatEvent(this.condition)}, originalValue=${this.originalValue.toString()}, delta=${this.delta.toString()}, expected=${this.expected.toString()}`; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Tokens/setComptrollerTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | makeComptroller, 3 | makeCToken 4 | } = require('../Utils/Compound'); 5 | 6 | describe('CToken', function () { 7 | let root, accounts; 8 | let cToken, oldComptroller, newComptroller; 9 | beforeEach(async () => { 10 | [root, ...accounts] = saddle.accounts; 11 | cToken = await makeCToken(); 12 | oldComptroller = cToken.comptroller; 13 | newComptroller = await makeComptroller(); 14 | expect(newComptroller._address).not.toEqual(oldComptroller._address); 15 | }); 16 | 17 | describe('_setComptroller', () => { 18 | it("should fail if called by non-admin", async () => { 19 | expect( 20 | await send(cToken, '_setComptroller', [newComptroller._address], { from: accounts[0] }) 21 | ).toHaveTokenFailure('UNAUTHORIZED', 'SET_COMPTROLLER_OWNER_CHECK'); 22 | expect(await call(cToken, 'comptroller')).toEqual(oldComptroller._address); 23 | }); 24 | 25 | it("reverts if passed a contract that doesn't implement isComptroller", async () => { 26 | await expect(send(cToken, '_setComptroller', [cToken.underlying._address])).rejects.toRevert("revert"); 27 | expect(await call(cToken, 'comptroller')).toEqual(oldComptroller._address); 28 | }); 29 | 30 | it("reverts if passed a contract that implements isComptroller as false", async () => { 31 | // extremely unlikely to occur, of course, but let's be exhaustive 32 | const badComptroller = await makeComptroller({ kind: 'false-marker' }); 33 | await expect(send(cToken, '_setComptroller', [badComptroller._address])).rejects.toRevert("revert marker method returned false"); 34 | expect(await call(cToken, 'comptroller')).toEqual(oldComptroller._address); 35 | }); 36 | 37 | it("updates comptroller and emits log on success", async () => { 38 | const result = await send(cToken, '_setComptroller', [newComptroller._address]); 39 | expect(result).toSucceed(); 40 | expect(result).toHaveLog('NewComptroller', { 41 | oldComptroller: oldComptroller._address, 42 | newComptroller: newComptroller._address 43 | }); 44 | expect(await call(cToken, 'comptroller')).toEqual(newComptroller._address); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/certora/contracts/GovernorAlphaCertora.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | pragma experimental ABIEncoderV2; 3 | 4 | import "../../../contracts/Governance/GovernorAlpha.sol"; 5 | 6 | contract GovernorAlphaCertora is GovernorAlpha { 7 | Proposal proposal; 8 | 9 | constructor(address timelock_, address comp_, address guardian_) GovernorAlpha(timelock_, comp_, guardian_) public {} 10 | 11 | // XXX breaks solver 12 | /* function certoraPropose() public returns (uint) { */ 13 | /* return propose(proposal.targets, proposal.values, proposal.signatures, proposal.calldatas, "Motion to do something"); */ 14 | /* } */ 15 | 16 | /* function certoraProposalLength(uint proposalId) public returns (uint) { */ 17 | /* return proposals[proposalId].targets.length; */ 18 | /* } */ 19 | 20 | function certoraProposalStart(uint proposalId) public returns (uint) { 21 | return proposals[proposalId].startBlock; 22 | } 23 | 24 | function certoraProposalEnd(uint proposalId) public returns (uint) { 25 | return proposals[proposalId].endBlock; 26 | } 27 | 28 | function certoraProposalEta(uint proposalId) public returns (uint) { 29 | return proposals[proposalId].eta; 30 | } 31 | 32 | function certoraProposalExecuted(uint proposalId) public returns (bool) { 33 | return proposals[proposalId].executed; 34 | } 35 | 36 | function certoraProposalState(uint proposalId) public returns (uint) { 37 | return uint(state(proposalId)); 38 | } 39 | 40 | function certoraProposalVotesFor(uint proposalId) public returns (uint) { 41 | return proposals[proposalId].forVotes; 42 | } 43 | 44 | function certoraProposalVotesAgainst(uint proposalId) public returns (uint) { 45 | return proposals[proposalId].againstVotes; 46 | } 47 | 48 | function certoraVoterVotes(uint proposalId, address voter) public returns (uint) { 49 | return proposals[proposalId].receipts[voter].votes; 50 | } 51 | 52 | function certoraTimelockDelay() public returns (uint) { 53 | return timelock.delay(); 54 | } 55 | 56 | function certoraTimelockGracePeriod() public returns (uint) { 57 | return timelock.GRACE_PERIOD(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /script/saddle/deployToken.js: -------------------------------------------------------------------------------- 1 | let { loadConf } = require('./support/tokenConfig'); 2 | 3 | function printUsage() { 4 | console.log(` 5 | usage: npx saddle script token:deploy {tokenConfig} 6 | 7 | note: pass VERIFY=true and ETHERSCAN_API_KEY= to verify contract on Etherscan 8 | 9 | example: 10 | 11 | npx saddle -n rinkeby script token:deploy '{ 12 | "underlying": "0x577D296678535e4903D59A4C929B718e1D575e0A", 13 | "comptroller": "$Comptroller", 14 | "interestRateModel": "$Base200bps_Slope3000bps", 15 | "initialExchangeRateMantissa": "2.0e18", 16 | "name": "Compound Kyber Network Crystal", 17 | "symbol": "cKNC", 18 | "decimals": "8", 19 | "admin": "$Timelock" 20 | }' 21 | `); 22 | } 23 | 24 | function sleep(timeout) { 25 | return new Promise((resolve, reject) => { 26 | setTimeout(() => { 27 | resolve(); 28 | }, timeout); 29 | }); 30 | } 31 | 32 | (async function() { 33 | if (args.length !== 1) { 34 | return printUsage(); 35 | } 36 | 37 | let conf = loadConf(args[0], addresses); 38 | if (!conf) { 39 | return printUsage(); 40 | } 41 | 42 | console.log(`Deploying cToken with ${JSON.stringify(conf)}`); 43 | 44 | let deployArgs = [conf.underlying, conf.comptroller, conf.interestRateModel, conf.initialExchangeRateMantissa.toString(), conf.name, conf.symbol, conf.decimals, conf.admin]; 45 | let contract = await saddle.deploy('CErc20Immutable', deployArgs); 46 | 47 | console.log(`Deployed contract to ${contract._address}`); 48 | 49 | if (env['VERIFY']) { 50 | const etherscanApiKey = env['ETHERSCAN_API_KEY']; 51 | if (etherscanApiKey === undefined || etherscanApiKey.length === 0) { 52 | throw new Error(`ETHERSCAN_API_KEY must be set if using VERIFY flag...`); 53 | } 54 | 55 | console.log(`Sleeping for 30 seconds then verifying contract on Etherscan...`); 56 | await sleep(30000); // Give Etherscan time to learn about contract 57 | console.log(`Now verifying contract on Etherscan...`); 58 | 59 | await saddle.verify(etherscanApiKey, contract._address, 'CErc20Immutable', deployArgs, 0); 60 | console.log(`Contract verified at https://${network}.etherscan.io/address/${contract._address}`); 61 | } 62 | 63 | return { 64 | ...conf, 65 | address: contract._address 66 | }; 67 | })(); 68 | -------------------------------------------------------------------------------- /tests/Tokens/transferTest.js: -------------------------------------------------------------------------------- 1 | const {makeCToken} = require('../Utils/Compound'); 2 | 3 | describe('CToken', function () { 4 | let root, accounts; 5 | beforeEach(async () => { 6 | [root, ...accounts] = saddle.accounts; 7 | }); 8 | 9 | describe('transfer', () => { 10 | it("cannot transfer from a zero balance", async () => { 11 | const cToken = await makeCToken({supportMarket: true}); 12 | expect(await call(cToken, 'balanceOf', [root])).toEqualNumber(0); 13 | expect(await send(cToken, 'transfer', [accounts[0], 100])).toHaveTokenFailure('MATH_ERROR', 'TRANSFER_NOT_ENOUGH'); 14 | }); 15 | 16 | it("transfers 50 tokens", async () => { 17 | const cToken = await makeCToken({supportMarket: true}); 18 | await send(cToken, 'harnessSetBalance', [root, 100]); 19 | expect(await call(cToken, 'balanceOf', [root])).toEqualNumber(100); 20 | await send(cToken, 'transfer', [accounts[0], 50]); 21 | expect(await call(cToken, 'balanceOf', [root])).toEqualNumber(50); 22 | expect(await call(cToken, 'balanceOf', [accounts[0]])).toEqualNumber(50); 23 | }); 24 | 25 | it("doesn't transfer when src == dst", async () => { 26 | const cToken = await makeCToken({supportMarket: true}); 27 | await send(cToken, 'harnessSetBalance', [root, 100]); 28 | expect(await call(cToken, 'balanceOf', [root])).toEqualNumber(100); 29 | expect(await send(cToken, 'transfer', [root, 50])).toHaveTokenFailure('BAD_INPUT', 'TRANSFER_NOT_ALLOWED'); 30 | }); 31 | 32 | it("rejects transfer when not allowed and reverts if not verified", async () => { 33 | const cToken = await makeCToken({comptrollerOpts: {kind: 'bool'}}); 34 | await send(cToken, 'harnessSetBalance', [root, 100]); 35 | expect(await call(cToken, 'balanceOf', [root])).toEqualNumber(100); 36 | 37 | await send(cToken.comptroller, 'setTransferAllowed', [false]) 38 | expect(await send(cToken, 'transfer', [root, 50])).toHaveTrollReject('TRANSFER_COMPTROLLER_REJECTION'); 39 | 40 | await send(cToken.comptroller, 'setTransferAllowed', [true]) 41 | await send(cToken.comptroller, 'setTransferVerify', [false]) 42 | await expect(send(cToken, 'transfer', [accounts[0], 50])).rejects.toRevert("revert transferVerify rejected transfer"); 43 | }); 44 | }); 45 | }); -------------------------------------------------------------------------------- /scenario/src/Web.ts: -------------------------------------------------------------------------------- 1 | import { parse } from './Parser'; 2 | import { World, initWorld } from './World'; 3 | import { throwExpect } from './Assert'; 4 | import { CallbackPrinter } from './Printer'; 5 | import { runCommand } from './Runner'; 6 | import { loadContractData, parseNetworkFile } from './Networks'; 7 | import Web3 from 'web3'; 8 | import { Saddle } from 'eth-saddle'; 9 | 10 | function networkFromId(id: number) { 11 | switch (id) { 12 | case 0: 13 | return 'olympic'; 14 | 15 | case 1: 16 | return 'mainnet'; 17 | 18 | case 2: 19 | return 'morden'; 20 | 21 | case 3: 22 | return 'ropsten'; 23 | 24 | case 4: 25 | return 'rinkeby'; 26 | 27 | case 5: 28 | return 'goerli'; 29 | 30 | case 8: 31 | return 'ubiq'; 32 | 33 | case 42: 34 | return 'kovan'; 35 | 36 | case 77: 37 | return 'sokol'; 38 | 39 | case 99: 40 | return 'core'; 41 | 42 | case 999: 43 | return 'development'; 44 | 45 | default: 46 | return ''; 47 | } 48 | } 49 | 50 | export async function webWorld( 51 | web3: Web3, 52 | networksData: string, 53 | networksABIData: string, 54 | printerCallback: (message: any) => void 55 | ): Promise { 56 | let printer = new CallbackPrinter(printerCallback); 57 | let accounts; 58 | if (web3.currentProvider && typeof(web3.currentProvider) !== 'string') { 59 | // XXXS 60 | accounts = [(web3.currentProvider).address]; 61 | } 62 | 63 | const networkId = await (web3 as any).net.getId(); 64 | const network: string = networkFromId(networkId); 65 | 66 | // XXXS 67 | const saddle = { 68 | web3: web3 69 | }; 70 | 71 | let world = await initWorld(throwExpect, printer, web3, saddle, network, accounts, null, null); 72 | 73 | let networks = parseNetworkFile(networksData); 74 | let networksABI = parseNetworkFile(networksABIData); 75 | 76 | [world] = await loadContractData(world, networks, networksABI); 77 | // world = loadInvokationOpts(world); 78 | // world = loadVerbose(world); 79 | // world = loadDryRun(world); 80 | // world = await loadSettings(world); 81 | 82 | return world; 83 | } 84 | 85 | export async function webParse(world: World, line: string): Promise { 86 | return runCommand(world, line, {}); 87 | } 88 | -------------------------------------------------------------------------------- /spec/certora/Comp/transfer.cvl: -------------------------------------------------------------------------------- 1 | transferDelegates(address src, address dst, uint amount) { 2 | env e0; 3 | env e1; 4 | require e1.block.number > e0.block.number; 5 | 6 | address srcRep = sinvoke delegates(e0, src); 7 | address dstRep = sinvoke delegates(e0, dst); 8 | 9 | uint srcBalancePrior = sinvoke balanceOf(e0, src); 10 | uint dstBalancePrior = sinvoke balanceOf(e0, dst); 11 | 12 | uint srcVotesPrior = sinvoke getCurrentVotes(e0, srcRep); 13 | uint dstVotesPrior = sinvoke getCurrentVotes(e0, dstRep); 14 | 15 | // Bound the number of checkpoints to prevent solver timeout / unwinding assertion violation 16 | uint32 nCheckpoints; 17 | require nCheckpoints == 1; // XXX 18 | require invoke numCheckpoints(e0, srcRep) == nCheckpoints && invoke numCheckpoints(e0, dstRep) == nCheckpoints; 19 | 20 | // Ensure the checkpoints are sane 21 | require sinvoke certoraOrdered(e0, src); 22 | require sinvoke certoraOrdered(e0, dst); 23 | require srcVotesPrior >= srcBalancePrior; 24 | require dstVotesPrior >= dstBalancePrior; 25 | 26 | require amount <= 79228162514264337593543950335; // UInt96 Max 27 | bool didTransfer = invoke transferFrom(e0, src, dst, amount); 28 | bool transferReverted = lastReverted; 29 | assert didTransfer || transferReverted, "Transfer either succeeds or reverts"; 30 | 31 | uint srcBalancePost = sinvoke balanceOf(e1, src); 32 | uint dstBalancePost = sinvoke balanceOf(e1, dst); 33 | assert !transferReverted => ( 34 | (src != dst) => ((dstBalancePost == dstBalancePrior + amount) && (srcBalancePost == srcBalancePrior - amount)) && 35 | (src == dst) => ((dstBalancePost == dstBalancePrior) && (srcBalancePost == srcBalancePrior)) 36 | ), "Transfers right amount"; 37 | 38 | uint srcVotesPost = sinvoke getCurrentVotes(e1, srcRep); 39 | uint dstVotesPost = sinvoke getCurrentVotes(e1, dstRep); 40 | 41 | assert (srcRep == 0 && dstRep != 0 && !transferReverted) => (dstVotesPost == dstVotesPrior + amount), "Votes are created from the abyss"; 42 | assert (srcRep != 0 && dstRep == 0 && !transferReverted) => (srcVotesPost + amount == srcVotesPrior), "Votes are destroyed into the abyss"; 43 | assert (srcRep != 0 && dstRep != 0) => (srcVotesPrior + dstVotesPrior == srcVotesPost + dstVotesPost), "Votes are neither created nor destroyed"; 44 | } -------------------------------------------------------------------------------- /spec/scenario/Governor/Defeat.scen: -------------------------------------------------------------------------------- 1 | Macro DeployGov 2 | SetBlockNumber 1 3 | Counter Deploy CNT1 4 | Timelock Deploy Scenario Jared 604800 5 | Comp Deploy Bank 6 | Governor Deploy Alpha LegitGov (Address Timelock) (Address Comp) Guardian 7 | Timelock SetAdmin (Address LegitGov) 8 | Enfranchise Root 100001e18 9 | Enfranchise Jared 200000e18 10 | Enfranchise Torrey 600001e18 11 | Enfranchise Geoff 700001e18 12 | 13 | Macro Enfranchise user amount 14 | From Bank (Comp Transfer user amount) 15 | From user (Comp Delegate user) 16 | 17 | Macro GivenPendingProposal 18 | DeployGov 19 | MineBlock 20 | MineBlock 21 | Governor LegitGov Propose "Add and sub" [(Address CNT1) (Address CNT1)] [0 0] ["increment(uint256,uint256)" "decrement(uint256)"] [["7" "4"] ["2"]] 22 | Assert Equal ("Pending") (Governor LegitGov Proposal LastProposal State) 23 | 24 | Macro GivenActiveProposal 25 | GivenPendingProposal 26 | MineBlock 27 | MineBlock 28 | Assert Equal ("Active") (Governor LegitGov Proposal LastProposal State) 29 | 30 | Test "Defeat when for votes do not reach quorum" 31 | GivenActiveProposal 32 | Governor LegitGov Proposal LastProposal Vote For 33 | AdvanceBlocks 20000 34 | Assert Equal ("Defeated") (Governor LegitGov Proposal LastProposal State) 35 | 36 | Test "Defeat when more against votes than for votes" 37 | GivenActiveProposal 38 | From Torrey (Governor LegitGov Proposal LastProposal Vote For ) 39 | From Geoff (Governor LegitGov Proposal LastProposal Vote Against ) 40 | AdvanceBlocks 20000 41 | Assert Equal ("Defeated") (Governor LegitGov Proposal LastProposal State) 42 | 43 | Test "(not defeat) when vote is ongoing" 44 | GivenActiveProposal 45 | From Torrey (Governor LegitGov Proposal LastProposal Vote For ) 46 | From Geoff (Governor LegitGov Proposal LastProposal Vote For ) 47 | Assert Equal ("Active") (Governor LegitGov Proposal LastProposal State) 48 | 49 | Test "(not defeat) when fors pass quorum and nays" 50 | GivenActiveProposal 51 | From Torrey (Governor LegitGov Proposal LastProposal Vote For ) 52 | From Geoff (Governor LegitGov Proposal LastProposal Vote For ) 53 | From Jared (Governor LegitGov Proposal LastProposal Vote Against ) 54 | AdvanceBlocks 20000 55 | Assert Equal ("Succeeded") (Governor LegitGov Proposal LastProposal State) 56 | -------------------------------------------------------------------------------- /scenario/src/Value/MCDValue.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../Event'; 2 | import { World } from '../World'; 3 | import { getContract } from '../Contract'; 4 | import { Pot } from '../Contract/Pot'; 5 | import { Vat } from '../Contract/Vat'; 6 | import { 7 | getAddressV, 8 | getCoreValue, 9 | getStringV 10 | } from '../CoreValue'; 11 | import { Arg, Fetcher, getFetcherValue } from '../Command'; 12 | import { 13 | AddressV, 14 | NumberV, 15 | Value, 16 | StringV 17 | } from '../Value'; 18 | 19 | export function mcdFetchers() { 20 | return [ 21 | new Fetcher<{ potAddress: AddressV, method: StringV, args: StringV[] }, Value>(` 22 | #### PotAt 23 | 24 | * "MCD PotAt " 25 | * E.g. "MCD PotAt "0xPotAddress" "pie" (CToken cDai Address)" 26 | `, 27 | "PotAt", 28 | [ 29 | new Arg("potAddress", getAddressV), 30 | new Arg("method", getStringV), 31 | new Arg('args', getCoreValue, { variadic: true, mapped: true }) 32 | ], 33 | async (world, { potAddress, method, args }) => { 34 | const PotContract = getContract('PotLike'); 35 | const pot = await PotContract.at(world, potAddress.val); 36 | const argStrings = args.map(arg => arg.val); 37 | return new NumberV(await pot.methods[method.val](...argStrings).call()) 38 | } 39 | ), 40 | 41 | new Fetcher<{ vatAddress: AddressV, method: StringV, args: StringV[] }, Value>(` 42 | #### VatAt 43 | 44 | * "MCD VatAt " 45 | * E.g. "MCD VatAt "0xVatAddress" "dai" (CToken cDai Address)" 46 | `, 47 | "VatAt", 48 | [ 49 | new Arg("vatAddress", getAddressV), 50 | new Arg("method", getStringV), 51 | new Arg('args', getCoreValue, { variadic: true, mapped: true }) 52 | ], 53 | async (world, { vatAddress, method, args }) => { 54 | const VatContract = getContract('VatLike'); 55 | const vat = await VatContract.at(world, vatAddress.val); 56 | const argStrings = args.map(arg => arg.val); 57 | return new NumberV(await vat.methods[method.val](...argStrings).call()) 58 | } 59 | ) 60 | ]; 61 | } 62 | 63 | export async function getMCDValue(world: World, event: Event): Promise { 64 | return await getFetcherValue("MCD", mcdFetchers(), world, event); 65 | } 66 | -------------------------------------------------------------------------------- /spec/scenario/Seize.scen: -------------------------------------------------------------------------------- 1 | 2 | Test "Fail to seize calling directly" 3 | NewComptroller 4 | ListedCToken ZRX cZRX initialExchangeRate:1e9 5 | ListedCToken BAT cBAT initialExchangeRate:1e9 6 | Prep Geoff Some ZRX cZRX 7 | Mint Geoff 50e18 cZRX 8 | Invariant Remains (Erc20 cZRX TokenBalance Geoff) 50e9 9 | AllowFailures 10 | Seize 1e9 cZRX caller:Geoff liquidator:Geoff borrower:Torrey 11 | -- The caller must be from another cToken market, thus this fails 12 | Assert Failure COMPTROLLER_REJECTION LIQUIDATE_SEIZE_COMPTROLLER_REJECTION MARKET_NOT_LISTED 13 | 14 | Test "Seize tokens with a paused WBTC cToken-- like normal" 15 | NewComptroller 16 | ListedCToken ZRX cZRX initialExchangeRate:1e9 17 | ListedCToken WBTC cWBTC initialExchangeRate:0.1 tokenType:WBTC 18 | Prep Geoff Some ZRX cZRX 19 | Mint Geoff 50e18 cZRX 20 | Erc20 WBTC Pause 21 | Invariant Remains (Erc20 cZRX TokenBalance Geoff) 50e9 22 | AllowFailures 23 | Seize 1e9 cWBTC caller:Geoff liquidator:Geoff borrower:Torrey 24 | -- The caller must be from another cToken market, thus this fails 25 | Assert Failure COMPTROLLER_REJECTION LIQUIDATE_SEIZE_COMPTROLLER_REJECTION MARKET_NOT_LISTED 26 | 27 | Test "Not able to seize tokens with a malicious unlisted cToken" 28 | NewComptroller 29 | ListedCToken ZRX cZRX initialExchangeRate:1e9 30 | NewCTokenImmutable EVL cEVL initialExchangeRate:1e9 cTokenType:CEvil 31 | Prep Geoff Some ZRX cZRX 32 | Mint Geoff 50e18 cZRX 33 | Invariant Remains (Erc20 cZRX TokenBalance Geoff) 50e9 34 | Invariant Static (Erc20 cZRX TokenBalance Geoff) 35 | Invariant Static (Erc20 cZRX TokenBalance Torrey) 36 | AllowFailures 37 | EvilSeize cEVL 1e9 cZRX seizer:Geoff seizee:Torrey 38 | -- The caller must be from another cToken market, thus this fails 39 | Assert Failure COMPTROLLER_REJECTION LIQUIDATE_SEIZE_COMPTROLLER_REJECTION MARKET_NOT_LISTED 40 | 41 | Test "Able to seize tokens with a malicious listed cToken" 42 | NewComptroller 43 | ListedCToken ZRX cZRX initialExchangeRate:1e9 44 | ListedCTokenImmutable EVL cEVL initialExchangeRate:1e9 cTokenType:CEvil 45 | Prep Geoff Some ZRX cZRX 46 | Mint Geoff 50e18 cZRX 47 | Assert Equal (Erc20 cZRX TokenBalance Geoff) 50e9 48 | Expect Changes (Erc20 cZRX TokenBalance Geoff) -1e9 49 | Expect Changes (Erc20 cZRX TokenBalance Torrey) +1e9 50 | EvilSeize cEVL 1e9 cZRX seizer:Torrey seizee:Geoff 51 | -------------------------------------------------------------------------------- /spec/scenario/BorrowWBTC.scen: -------------------------------------------------------------------------------- 1 | 2 | Test "Borrow some WBTC enters WBTC and succeeds when not entered" 3 | Invariant Success 4 | NewComptroller price:1.0 5 | NewCToken ZRX cZRX 6 | NewCToken WBTC cWBTC tokenType:WBTC 7 | Give cWBTC 10e8 WBTC -- Faucet some WBTC to borrow 8 | Support cZRX collateralFactor:0.5 9 | Support cWBTC collateralFactor:0.5 10 | Prep Geoff Some ZRX cZRX 11 | Mint Geoff 100e18 cZRX 12 | EnterMarkets Geoff cZRX 13 | Borrow Geoff 1e8 cWBTC 14 | Assert Equal (cToken cWBTC BorrowBalance Geoff) (Exactly 1e8) 15 | Assert Equal (Erc20 WBTC TokenBalance Geoff) (Exactly 1e8) 16 | Assert Equal (Erc20 WBTC TokenBalance cWBTC) (Exactly 9e8) 17 | 18 | Test "Borrow some WBTC fails when no WBTC available" 19 | NewComptroller price:1.0 20 | NewCToken ZRX cZRX 21 | NewCToken WBTC cWBTC tokenType:WBTC 22 | Support cZRX collateralFactor:0.5 23 | Support cWBTC collateralFactor:0.5 24 | Prep Geoff Some ZRX cZRX 25 | Mint Geoff 100e18 cZRX 26 | EnterMarkets Geoff cZRX cWBTC 27 | Invariant Static (CToken cZRX ExchangeRateStored) 28 | AllowFailures 29 | Borrow Geoff 1e8 cWBTC 30 | Assert Failure TOKEN_INSUFFICIENT_CASH BORROW_CASH_NOT_AVAILABLE 31 | 32 | Test "Borrow some WBTC fails when WBTC paused" 33 | NewComptroller price:1.0 34 | NewCToken ZRX cZRX 35 | NewCToken WBTC cWBTC tokenType:WBTC 36 | Give cWBTC 10e8 WBTC -- Faucet some WBTC to borrow 37 | Support cZRX collateralFactor:0.5 38 | Support cWBTC collateralFactor:0.5 39 | Prep Geoff Some ZRX cZRX 40 | Mint Geoff 100e18 cZRX 41 | EnterMarkets Geoff cZRX cWBTC 42 | Invariant Static (CToken cZRX ExchangeRateStored) 43 | Erc20 WBTC Pause 44 | AllowFailures 45 | Borrow Geoff 1e8 cWBTC 46 | Assert Revert 47 | 48 | Test "Borrow some WBTC from Excess Cash" 49 | Invariant Success 50 | NewComptroller price:1.0 51 | NewCToken ZRX cZRX 52 | NewCToken WBTC cWBTC tokenType:WBTC 53 | Give cWBTC 10e8 WBTC -- Faucet some WBTC to borrow 54 | Support cZRX collateralFactor:0.5 55 | Support cWBTC collateralFactor:0.5 56 | Prep Geoff Some ZRX cZRX 57 | Mint Geoff 100e18 cZRX 58 | EnterMarkets Geoff cZRX cWBTC 59 | Borrow Geoff 1e8 cWBTC 60 | EnterMarkets Geoff cZRX cWBTC 61 | Assert Equal (cToken cWBTC BorrowBalance Geoff) (Exactly 1e8) 62 | Assert Equal (Erc20 WBTC TokenBalance Geoff) (Exactly 1e8) 63 | Assert Equal (Erc20 WBTC TokenBalance cWBTC) (Exactly 9e8) 64 | -------------------------------------------------------------------------------- /scenario/src/Help.ts: -------------------------------------------------------------------------------- 1 | import {Event} from './Event'; 2 | import {Expression} from './Command'; 3 | import {mustString} from './Utils'; 4 | import {Printer} from './Printer'; 5 | 6 | export function printHelp(printer: Printer, event: Event, expressions: Expression[], path: string[]=[]) { 7 | if (event.length === 0) { 8 | let banner; 9 | 10 | if (path.length === 0) { 11 | banner = ( 12 | ` 13 | ## Compound Command Runner 14 | 15 | The Compound Command Runner makes it easy to interact with Compound. You can input simple commands 16 | and it will construct Web3 calls to pull data or generate transactions. A list of available commands 17 | is included below. To dig further into a command run \`Help \`, such as \`Help From\` or for 18 | sub-commands run \`Help CToken\` or \`Help CToken Mint\`. 19 | `).trim(); 20 | } else { 21 | if (expressions.length > 0) { 22 | banner = `### ${path.join(" ")} Sub-Commands`; 23 | } 24 | } 25 | 26 | if (!!banner) { 27 | printer.printMarkdown(banner); 28 | } 29 | 30 | expressions.forEach((expression) => { 31 | printer.printMarkdown(`\n${expression.doc}`); 32 | if (expression.subExpressions.length > 0) { 33 | printer.printMarkdown(`For more information, run: \`Help ${path} ${expression.name}\``); 34 | } 35 | }); 36 | } else { 37 | const [first, ...rest] = event; 38 | const expressionName = mustString(first); 39 | 40 | let expression = expressions.find((expression) => expression.name.toLowerCase() === expressionName.toLowerCase()); 41 | 42 | if (expression) { 43 | if (rest.length === 0) { 44 | printer.printMarkdown(`${expression.doc}`); 45 | } 46 | 47 | printHelp(printer, rest, expression.subExpressions, path.concat(expression.name)); 48 | } else { 49 | let matchingExpressions = expressions.filter((expression) => expression.name.toLowerCase().startsWith(expressionName.toLowerCase())); 50 | 51 | if (matchingExpressions.length === 0) { 52 | printer.printLine(`\nError: cannot find help docs for ${path.concat(first).join(" ")}`); 53 | } else { 54 | if (rest.length === 0) { 55 | matchingExpressions.forEach((expression) => { 56 | printer.printMarkdown(`${expression.doc}`); 57 | }); 58 | } else { 59 | printer.printLine(`\nError: cannot find help docs for ${path.concat(event).join(" ")}`); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scenario/src/Settings.ts: -------------------------------------------------------------------------------- 1 | import { getNetworkPath, readFile, writeFile } from './File'; 2 | 3 | export class Settings { 4 | basePath: string | null; 5 | network: string | null; 6 | aliases: { [name: string]: string }; 7 | from: string | undefined; 8 | printTxLogs: boolean = false; 9 | 10 | constructor( 11 | basePath: string | null, 12 | network: string | null, 13 | aliases: { [name: string]: string }, 14 | from?: string 15 | ) { 16 | this.basePath = basePath; 17 | this.network = network; 18 | this.aliases = aliases; 19 | this.from = from; 20 | } 21 | 22 | static deserialize(basePath: string, network: string, data: string): Settings { 23 | const { aliases } = JSON.parse(data); 24 | 25 | return new Settings(basePath, network, aliases); 26 | } 27 | 28 | serialize(): string { 29 | return JSON.stringify({ 30 | aliases: this.aliases 31 | }); 32 | } 33 | 34 | static default(basePath: string | null, network: string | null): Settings { 35 | return new Settings(basePath, network, {}); 36 | } 37 | 38 | static getFilePath(basePath: string | null, network: string): string { 39 | return getNetworkPath(basePath, network, '-settings'); 40 | } 41 | 42 | static load(basePath: string, network: string): Promise { 43 | return readFile(null, Settings.getFilePath(basePath, network), Settings.default(basePath, network), data => 44 | Settings.deserialize(basePath, network, data) 45 | ); 46 | } 47 | 48 | async save(): Promise { 49 | if (this.network) { 50 | await writeFile(null, Settings.getFilePath(this.basePath, this.network), this.serialize()); 51 | } 52 | } 53 | 54 | lookupAlias(address: string): string { 55 | let entry = Object.entries(this.aliases).find(([key, value]) => { 56 | return value === address; 57 | }); 58 | 59 | if (entry) { 60 | return entry[0]; 61 | } else { 62 | return address; 63 | } 64 | } 65 | 66 | lookupAliases(address: string): string[] { 67 | let entries = Object.entries(this.aliases).filter(([key, value]) => { 68 | return value === address; 69 | }); 70 | 71 | return entries.map(([key, _value]) => key); 72 | } 73 | 74 | findAlias(name: string): string | null { 75 | const alias = Object.entries(this.aliases).find( 76 | ([alias, addr]) => alias.toLowerCase() === name.toLowerCase() 77 | ); 78 | 79 | if (alias) { 80 | return alias[1]; 81 | } else { 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Utils/InfuraProxy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --max-old-space-size=32768 2 | 3 | // Very Dumb Proxy 4 | // - will slow you down for uncached responses 5 | // - will crash when it runs out of memory (although now runs with 32GB heap by default) 6 | // - does not handle block number in url gracefully yet 7 | // 8 | // Run the proxy server e.g.: 9 | // $ tests/Utils/InfuraProxy.js 10 | // 11 | // Replace in Web3Fork command, or use from curl e.g.: 12 | // $ curl -X POST localhost:1337/kovan/367b617143a94994b4f9c20e36c31839 \ 13 | // --data "{\"jsonrpc\": \"2.0\", \"method\": \"eth_blockNumber\", \"params\": [], \"id\": 1}" 14 | // {"jsonrpc":"2.0","id":1,"result":"0x10675d7"} 15 | 16 | const http = require('http'); 17 | const https = require('https'); 18 | 19 | const port = 1337; 20 | const server = http.createServer(handle).listen(port); 21 | const cache = {}; 22 | 23 | async function handle(req, res) { 24 | let data = '' 25 | req.on('data', (d) => data += d); 26 | req.on('end', async () => { 27 | const [network, project] = req.url.substr(1).split('/'); 28 | const query = JSON.parse(data), id = query.id; delete query['id']; 29 | const key = `${req.url}:${JSON.stringify(query)}`; 30 | const hit = cache[key]; 31 | if (hit) { 32 | console.log('Cache hit...', network, req.method, project, data); 33 | const reply = JSON.parse(hit); reply.id = id; 34 | res.writeHead(200, {'Content-Type': 'application/javascript'}); 35 | res.end(JSON.stringify(reply)); 36 | } else { 37 | try { 38 | console.log('Requesting...', network, req.method, project, data); 39 | const result = await fetch({ 40 | host: `${network}.infura.io`, 41 | method: req.method, 42 | path: `/v3/${project}`, 43 | data: data 44 | }); 45 | res.writeHead(200, {'Content-Type': 'application/javascript'}); 46 | res.end(cache[key] = result); 47 | } catch (e) { 48 | console.error(e) 49 | res.writeHead(500, {'Content-Type': 'application/javascript'}); 50 | res.end(JSON.stringify({error: 'request failed'})); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | async function fetch(options) { 57 | let data = '' 58 | return new Promise( 59 | (okay, fail) => { 60 | const req = https.request(options, (res) => { 61 | res.on('data', (d) => data += d); 62 | res.on('end', () => okay(data)); 63 | }); 64 | req.on('error', (e) => fail(e)); 65 | req.end(options.data); 66 | }); 67 | } 68 | 69 | -------------------------------------------------------------------------------- /scenario/src/Value/InterestRateModelValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import {InterestRateModel} from '../Contract/InterestRateModel'; 4 | import { 5 | getAddressV, 6 | getNumberV 7 | } from '../CoreValue'; 8 | import { 9 | AddressV, 10 | NumberV, 11 | Value} from '../Value'; 12 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 13 | import {getInterestRateModel} from '../ContractLookup'; 14 | 15 | export async function getInterestRateModelAddress(world: World, interestRateModel: InterestRateModel): Promise { 16 | return new AddressV(interestRateModel._address); 17 | } 18 | 19 | export async function getBorrowRate(world: World, interestRateModel: InterestRateModel, cash: NumberV, borrows: NumberV, reserves: NumberV): Promise { 20 | return new NumberV(await interestRateModel.methods.getBorrowRate(cash.encode(), borrows.encode(), reserves.encode()).call(), 1.0e18 / 2102400); 21 | } 22 | 23 | export function interestRateModelFetchers() { 24 | return [ 25 | new Fetcher<{interestRateModel: InterestRateModel}, AddressV>(` 26 | #### Address 27 | 28 | * " Address" - Gets the address of the interest rate model 29 | * E.g. "InterestRateModel MyInterestRateModel Address" 30 | `, 31 | "Address", 32 | [ 33 | new Arg("interestRateModel", getInterestRateModel) 34 | ], 35 | (world, {interestRateModel}) => getInterestRateModelAddress(world, interestRateModel), 36 | {namePos: 1} 37 | ), 38 | 39 | new Fetcher<{interestRateModel: InterestRateModel, cash: NumberV, borrows: NumberV, reserves: NumberV}, NumberV>(` 40 | #### BorrowRate 41 | 42 | * " BorrowRate" - Gets the borrow rate of the interest rate model 43 | * E.g. "InterestRateModel MyInterestRateModel BorrowRate 0 10 0" 44 | `, 45 | "BorrowRate", 46 | [ 47 | new Arg("interestRateModel", getInterestRateModel), 48 | new Arg("cash", getNumberV), 49 | new Arg("borrows", getNumberV), 50 | new Arg("reserves", getNumberV) 51 | ], 52 | (world, {interestRateModel, cash, borrows, reserves}) => getBorrowRate(world, interestRateModel, cash, borrows, reserves), 53 | {namePos: 1} 54 | ) 55 | ]; 56 | } 57 | 58 | export async function getInterestRateModelValue(world: World, event: Event): Promise { 59 | return await getFetcherValue("InterestRateModel", interestRateModelFetchers(), world, event); 60 | } 61 | -------------------------------------------------------------------------------- /spec/scenario/Supply.scen.old: -------------------------------------------------------------------------------- 1 | -- Supply Tests 2 | 3 | Test "Geoff supplies Ether and we check 2 future balances and then supply again" 4 | AddToken Ether 5 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 6 | Approve Geoff Ether "10.0e18" 7 | Faucet Geoff Ether "10.0e18" 8 | Supply Geoff Ether "3e18" 9 | Assert Success 10 | FastForward 2 Blocks 11 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "6.0e18") -- 3 * ( 1 + 2 * .5 ) 12 | FastForward 2 Blocks 13 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "9.0e18") -- 3 * ( 1 + 4 * .5 ) 14 | Supply Geoff Ether "1e18" 15 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "10.0e18") -- 3 * ( 1 + 4 * .5 ) + 1 16 | FastForward 2 Blocks 17 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "20.0e18") -- 10 * ( 1 + 2 * .5 ) 18 | 19 | Test "Geoff supplies Ether, Torrey supplies Ether and then Geoff supplies more Ether" 20 | AddToken Ether 21 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 22 | Approve Geoff Ether "10.0e18" 23 | Faucet Geoff Ether "10.0e18" 24 | Approve Torrey Ether "5.0e18" 25 | Faucet Torrey Ether "5.0e18" 26 | Supply Geoff Ether "1e18" 27 | Assert Success 28 | FastForward 2 Blocks 29 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "2.0e18") 30 | Supply Torrey Ether "3e18" 31 | Assert Success 32 | FastForward 2 Blocks 33 | Assert Equal (SupplyBalance Torrey Ether) (Exactly "6.0e18") 34 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "4.0e18") 35 | Supply Geoff Ether "1e18" 36 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "5.0e18") 37 | 38 | Test "Can't supply an 'initial' asset" 39 | AddToken Dragon 40 | Approve Geoff Dragon "10.0e18" 41 | Faucet Geoff Dragon "10.0e18" 42 | Supply Geoff Dragon "1e18" 43 | Assert Failure MARKET_NOT_LISTED SUPPLY_MARKET_NOT_LISTED 44 | 45 | Test "Can't supply when contract is paused" 46 | AddToken Ether 47 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) SimplePolicyHook 48 | Approve Geoff Ether 1.0e18 49 | Faucet Geoff Ether 0.4e18 50 | PolicyHook Ether (SetPaused True) 51 | Supply Geoff Ether 0.3e18 52 | Assert Failure COMPTROLLER_REJECTION SUPPLY_COMPTROLLER_REJECTION 1 53 | Assert Equal (SupplyBalance Geoff Ether) (Exactly "0e18") 54 | 55 | Test "With always paused policy hook, can't supply when contract is paused" 56 | AddToken Ether 57 | SupportMarket Ether (FixedPrice 1.0) (FixedRate 0.5 0.75) AlwaysPausedPolicyHook 58 | Supply Geoff Ether 0.3e18 59 | Assert Failure COMPTROLLER_REJECTION SUPPLY_COMPTROLLER_REJECTION 99 60 | -------------------------------------------------------------------------------- /contracts/CarefulMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.16; 2 | 3 | /** 4 | * @title Careful Math 5 | * @author Compound 6 | * @notice Derived from OpenZeppelin's SafeMath library 7 | * https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol 8 | */ 9 | contract CarefulMath { 10 | 11 | /** 12 | * @dev Possible error codes that we can return 13 | */ 14 | enum MathError { 15 | NO_ERROR, 16 | DIVISION_BY_ZERO, 17 | INTEGER_OVERFLOW, 18 | INTEGER_UNDERFLOW 19 | } 20 | 21 | /** 22 | * @dev Multiplies two numbers, returns an error on overflow. 23 | */ 24 | function mulUInt(uint a, uint b) internal pure returns (MathError, uint) { 25 | if (a == 0) { 26 | return (MathError.NO_ERROR, 0); 27 | } 28 | 29 | uint c = a * b; 30 | 31 | if (c / a != b) { 32 | return (MathError.INTEGER_OVERFLOW, 0); 33 | } else { 34 | return (MathError.NO_ERROR, c); 35 | } 36 | } 37 | 38 | /** 39 | * @dev Integer division of two numbers, truncating the quotient. 40 | */ 41 | function divUInt(uint a, uint b) internal pure returns (MathError, uint) { 42 | if (b == 0) { 43 | return (MathError.DIVISION_BY_ZERO, 0); 44 | } 45 | 46 | return (MathError.NO_ERROR, a / b); 47 | } 48 | 49 | /** 50 | * @dev Subtracts two numbers, returns an error on overflow (i.e. if subtrahend is greater than minuend). 51 | */ 52 | function subUInt(uint a, uint b) internal pure returns (MathError, uint) { 53 | if (b <= a) { 54 | return (MathError.NO_ERROR, a - b); 55 | } else { 56 | return (MathError.INTEGER_UNDERFLOW, 0); 57 | } 58 | } 59 | 60 | /** 61 | * @dev Adds two numbers, returns an error on overflow. 62 | */ 63 | function addUInt(uint a, uint b) internal pure returns (MathError, uint) { 64 | uint c = a + b; 65 | 66 | if (c >= a) { 67 | return (MathError.NO_ERROR, c); 68 | } else { 69 | return (MathError.INTEGER_OVERFLOW, 0); 70 | } 71 | } 72 | 73 | /** 74 | * @dev add a and b and then subtract c 75 | */ 76 | function addThenSubUInt(uint a, uint b, uint c) internal pure returns (MathError, uint) { 77 | (MathError err0, uint sum) = addUInt(a, b); 78 | 79 | if (err0 != MathError.NO_ERROR) { 80 | return (err0, 0); 81 | } 82 | 83 | return subUInt(sum, c); 84 | } 85 | } -------------------------------------------------------------------------------- /spec/scenario/PriceOracleProxy.scen: -------------------------------------------------------------------------------- 1 | Macro SetupPriceOracleProxy 2 | Unitroller Deploy 3 | PriceOracle Deploy Simple 4 | -- Update to G1 5 | ComptrollerImpl Deploy ScenarioG1 ScenComptrollerG1 6 | Unitroller SetPendingImpl ScenComptrollerG1 7 | PriceOracleProxy Deploy Admin (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) (Address Zero) (Address Zero) 8 | ComptrollerImpl ScenComptrollerG1 BecomeG1 (PriceOracleProxy Address) 0.1 20 9 | -- Update to G2 10 | ComptrollerImpl Deploy StandardG2 ComptrollerG2 11 | Unitroller SetPendingImpl ComptrollerG2 12 | ComptrollerImpl ComptrollerG2 BecomeG2 13 | -- Update to G* 14 | ComptrollerImpl Deploy Scenario ScenComptroller 15 | Unitroller SetPendingImpl ScenComptroller 16 | ComptrollerImpl ScenComptroller Become 1e18 [] 17 | NewEtherToken cETH 18 | NewCToken USDC cUSDC 19 | NewCToken SAI cSAI 20 | NewCToken DAI cDAI 21 | NewCToken USDT cUSDT 22 | Comptroller SupportMarket cETH 23 | Comptroller SupportMarket cUSDC 24 | Comptroller SupportMarket cSAI 25 | Comptroller SupportMarket cDAI 26 | Comptroller SupportMarket cUSDT 27 | PriceOracleProxy Deploy Admin (PriceOracle Address) (Address cETH) (Address cUSDC) (Address cSAI) (Address cDAI) (Address cUSDT) 28 | Comptroller SetPriceOracle (PriceOracleProxy Address) 29 | 30 | Test "uses address(2) for dai and address(1) for usdc" 31 | SetupPriceOracleProxy 32 | PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000001) 5740564708.572881 33 | PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000002) 0.005842307360923634 34 | Assert Equal (PriceOracleProxy Price cUSDC) 5740564708572881000000000000 35 | Assert Equal (PriceOracleProxy Price cDAI) 5842307360923634 36 | 37 | Test "sai price is dai price until set" 38 | SetupPriceOracleProxy 39 | PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000002) 0.005842307360923634 40 | Assert Equal (PriceOracleProxy Price cSAI) 5842307360923634 41 | PriceOracleProxy SetSaiPrice 0.006842307360923634 42 | Assert Equal (PriceOracleProxy Price cSAI) 6842307360923634 43 | 44 | Test "gets tether and usdc prices" 45 | SetupPriceOracleProxy 46 | PriceOracle SetDirectPrice (Address 0x0000000000000000000000000000000000000001) 5740564708.572881 47 | -- scaled to 1e30 bc both tokens have 6 decimals 48 | Assert Equal (PriceOracleProxy Price cUSDT) 5740564708572881000000000000 49 | Assert Equal (PriceOracleProxy Price cUSDC) 5740564708572881000000000000 50 | -------------------------------------------------------------------------------- /scenario/src/Value/PriceOracleProxyValue.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {World} from '../World'; 3 | import {PriceOracleProxy} from '../Contract/PriceOracleProxy'; 4 | import { 5 | getAddressV 6 | } from '../CoreValue'; 7 | import { 8 | AddressV, 9 | NumberV, 10 | Value} from '../Value'; 11 | import {Arg, Fetcher, getFetcherValue} from '../Command'; 12 | import {getPriceOracleProxy} from '../ContractLookup'; 13 | 14 | export async function getPriceOracleProxyAddress(world: World, priceOracleProxy: PriceOracleProxy): Promise { 15 | return new AddressV(priceOracleProxy._address); 16 | } 17 | 18 | export async function getV1PriceOracle(world: World, priceOracleProxy: PriceOracleProxy): Promise { 19 | return new AddressV(await priceOracleProxy.methods.v1PriceOracle().call()); 20 | } 21 | 22 | async function getPrice(world: World, priceOracleProxy: PriceOracleProxy, asset: string): Promise { 23 | return new NumberV(await priceOracleProxy.methods.getUnderlyingPrice(asset).call()); 24 | } 25 | 26 | export function priceOracleProxyFetchers() { 27 | return [ 28 | new Fetcher<{priceOracleProxy: PriceOracleProxy}, AddressV>(` 29 | #### V1PriceOracle 30 | 31 | * "V1PriceOracle" - Gets the address of the v1 Price 32 | `, 33 | "V1PriceOracle", 34 | [ 35 | new Arg("priceOracleProxy", getPriceOracleProxy, {implicit: true}) 36 | ], 37 | (world, {priceOracleProxy}) => getV1PriceOracle(world, priceOracleProxy) 38 | ), 39 | new Fetcher<{priceOracleProxy: PriceOracleProxy}, AddressV>(` 40 | #### Address 41 | 42 | * "Address" - Gets the address of the global price oracle 43 | `, 44 | "Address", 45 | [ 46 | new Arg("priceOracleProxy", getPriceOracleProxy, {implicit: true}) 47 | ], 48 | (world, {priceOracleProxy}) => getPriceOracleProxyAddress(world, priceOracleProxy) 49 | ), 50 | new Fetcher<{priceOracle: PriceOracleProxy, asset: AddressV}, NumberV>(` 51 | #### Price 52 | 53 | * "Price asset:
" - Gets the price of the given asset 54 | `, 55 | "Price", 56 | [ 57 | new Arg("priceOracle", getPriceOracleProxy, {implicit: true}), 58 | new Arg("asset", getAddressV) 59 | ], 60 | (world, {priceOracle, asset}) => getPrice(world, priceOracle, asset.val) 61 | ) 62 | ]; 63 | } 64 | 65 | export async function getPriceOracleProxyValue(world: World, event: Event): Promise { 66 | return await getFetcherValue("PriceOracle", priceOracleProxyFetchers(), world, event); 67 | } 68 | -------------------------------------------------------------------------------- /tests/MaximillionTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | etherBalance, 3 | etherGasCost, 4 | getContract 5 | } = require('./Utils/Ethereum'); 6 | 7 | const { 8 | makeComptroller, 9 | makeCToken, 10 | makePriceOracle, 11 | pretendBorrow, 12 | borrowSnapshot 13 | } = require('./Utils/Compound'); 14 | 15 | describe('Maximillion', () => { 16 | let root, borrower; 17 | let maximillion, cEther; 18 | beforeEach(async () => { 19 | [root, borrower] = saddle.accounts; 20 | cEther = await makeCToken({kind: "cether", supportMarket: true}); 21 | maximillion = await deploy('Maximillion', [cEther._address]); 22 | }); 23 | 24 | describe("constructor", () => { 25 | it("sets address of cEther", async () => { 26 | expect(await call(maximillion, "cEther")).toEqual(cEther._address); 27 | }); 28 | }); 29 | 30 | describe("repayBehalf", () => { 31 | it("refunds the entire amount with no borrows", async () => { 32 | const beforeBalance = await etherBalance(root); 33 | const result = await send(maximillion, "repayBehalf", [borrower], {value: 100}); 34 | const gasCost = await etherGasCost(result); 35 | const afterBalance = await etherBalance(root); 36 | expect(result).toSucceed(); 37 | expect(afterBalance).toEqualNumber(beforeBalance.sub(gasCost)); 38 | }); 39 | 40 | it("repays part of a borrow", async () => { 41 | await pretendBorrow(cEther, borrower, 1, 1, 150); 42 | const beforeBalance = await etherBalance(root); 43 | const result = await send(maximillion, "repayBehalf", [borrower], {value: 100}); 44 | const gasCost = await etherGasCost(result); 45 | const afterBalance = await etherBalance(root); 46 | const afterBorrowSnap = await borrowSnapshot(cEther, borrower); 47 | expect(result).toSucceed(); 48 | expect(afterBalance).toEqualNumber(beforeBalance.sub(gasCost).sub(100)); 49 | expect(afterBorrowSnap.principal).toEqualNumber(50); 50 | }); 51 | 52 | it("repays a full borrow and refunds the rest", async () => { 53 | await pretendBorrow(cEther, borrower, 1, 1, 90); 54 | const beforeBalance = await etherBalance(root); 55 | const result = await send(maximillion, "repayBehalf", [borrower], {value: 100}); 56 | const gasCost = await etherGasCost(result); 57 | const afterBalance = await etherBalance(root); 58 | const afterBorrowSnap = await borrowSnapshot(cEther, borrower); 59 | expect(result).toSucceed(); 60 | expect(afterBalance).toEqualNumber(beforeBalance.sub(gasCost).sub(90)); 61 | expect(afterBorrowSnap.principal).toEqualNumber(0); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/certora/CErc20/exchangeRate.cvl: -------------------------------------------------------------------------------- 1 | 2 | exchangeRateDecreases(uint result, address minter, uint256 mintAmount, uint256 mintTokens) { 3 | // Pre/action/post environments 4 | env e0; 5 | env e1; 6 | env e2; 7 | 8 | require e1.block.number >= e0.block.number; 9 | require e2.block.number >= e1.block.number; 10 | 11 | // Any other account 12 | address other; 13 | require other != minter && other != currentContract; 14 | require minter != currentContract; 15 | 16 | uint256 exchangeRatePre = sinvoke exchangeRateCurrent(e0); 17 | 18 | uint256 cTokenCashPre = sinvoke getCash(e0); 19 | uint256 cTokenBorrowsPre = sinvoke totalBorrows(e0); 20 | uint256 cTokenTokensPre = sinvoke totalSupply(e0); 21 | uint256 cTokenReservesPre = sinvoke totalReserves(e0); 22 | uint256 cTokenSupplyPre = cTokenCashPre + cTokenBorrowsPre - cTokenReservesPre; 23 | 24 | // Simplifying assumptions to analyze the vulnerability 25 | require cTokenBorrowsPre == 0; // XXX not necessary, simplifies analysis 26 | require cTokenSupplyPre >= 0; // XXX not necessary, should underflow 27 | require cTokenTokensPre > 0; // XXX not necessary 28 | require exchangeRatePre * cTokenTokensPre == cTokenSupplyPre * 1000000000000000000; // XXX 29 | 30 | // XXX why does this have to be after require? 31 | uint256 implicitExchangeRatePre = cTokenSupplyPre * 1000000000000000000 / cTokenTokensPre; 32 | 33 | require result == invoke mintFreshPub(e1, minter, mintAmount); 34 | bool mintFreshReverted = lastReverted; 35 | 36 | uint256 exchangeRatePost = sinvoke exchangeRateCurrent(e2); 37 | 38 | uint256 cTokenCashPost = sinvoke getCash(e2); 39 | uint256 cTokenBorrowsPost = sinvoke totalBorrows(e2); 40 | uint256 cTokenTokensPost = sinvoke totalSupply(e2); 41 | uint256 cTokenReservesPost = sinvoke totalReserves(e2); 42 | uint256 cTokenSupplyPost = cTokenCashPost + cTokenBorrowsPost - cTokenReservesPost; 43 | 44 | require mintTokens == cTokenTokensPost - cTokenTokensPre; 45 | require exchangeRatePre * mintTokens == mintAmount * 1000000000000000000; // XXX why would this need to be assumed? should be proven 46 | 47 | uint256 implicitExchangeRatePost = cTokenSupplyPost * 1000000000000000000 / cTokenTokensPost; 48 | 49 | assert (!mintFreshReverted => 50 | ((result != 0) => (exchangeRatePost == exchangeRatePre))), "Mismatch in failure case"; 51 | assert (!mintFreshReverted => (exchangeRatePost >= exchangeRatePre)), "Exchange rate decreased"; 52 | assert (!mintFreshReverted => (implicitExchangeRatePost >= implicitExchangeRatePre)), "Implicit exchange rate decreased"; 53 | } 54 | -------------------------------------------------------------------------------- /scenario/src/Event/ExpectationEvent.ts: -------------------------------------------------------------------------------- 1 | import {Event} from '../Event'; 2 | import {addExpectation, World} from '../World'; 3 | import { 4 | EventV, 5 | NumberV, 6 | Value 7 | } from '../Value'; 8 | import { 9 | getCoreValue, 10 | getEventV, 11 | getNumberV 12 | } from '../CoreValue'; 13 | import {Invariant} from '../Invariant'; 14 | import {ChangesExpectation} from '../Expectation/ChangesExpectation'; 15 | import {RemainsExpectation} from '../Expectation/RemainsExpectation'; 16 | import {formatEvent} from '../Formatter'; 17 | import {Arg, View, processCommandEvent} from '../Command'; 18 | 19 | async function changesExpectation(world: World, condition: Event, delta: NumberV, tolerance: NumberV): Promise { 20 | const value = await getCoreValue(world, condition); 21 | const expectation = new ChangesExpectation(condition, value, delta, tolerance); 22 | 23 | return addExpectation(world, expectation); 24 | } 25 | 26 | async function remainsExpectation(world: World, condition: Event, value: Value): Promise { 27 | const expectation = new RemainsExpectation(condition, value); 28 | 29 | // Immediately check value matches 30 | await expectation.checker(world, true); 31 | 32 | return addExpectation(world, expectation); 33 | } 34 | 35 | export function expectationCommands() { 36 | return [ 37 | new View<{condition: EventV, delta: NumberV, tolerance: NumberV}>(` 38 | #### Changes 39 | 40 | * "Changes amount: tolerance:" - Expects that given value changes by amount 41 | * E.g ."Expect Changes (CToken cZRX UnderlyingBalance Geoff) +10e18" 42 | `, 43 | "Changes", 44 | [ 45 | new Arg("condition", getEventV), 46 | new Arg("delta", getNumberV), 47 | new Arg("tolerance", getNumberV, {default: new NumberV(0)}) 48 | ], 49 | (world, {condition, delta, tolerance}) => changesExpectation(world, condition.val, delta, tolerance) 50 | ), 51 | 52 | new View<{condition: EventV, value: Value}>(` 53 | #### Remains 54 | 55 | * "Expect Remains " - Ensures that the given condition starts at and remains a given value 56 | * E.g ."Expect Remains (CToken cZRX UnderlyingBalance Geoff) (Exactly 0)" 57 | `, 58 | "Remains", 59 | [ 60 | new Arg("condition", getEventV), 61 | new Arg("value", getCoreValue) 62 | ], 63 | (world, {condition, value}) => remainsExpectation(world, condition.val, value) 64 | ) 65 | ]; 66 | } 67 | 68 | export async function processExpectationEvent(world: World, event: Event, from: string | null): Promise { 69 | return await processCommandEvent("Expectation", expectationCommands(), world, event, from); 70 | } 71 | -------------------------------------------------------------------------------- /scenario/src/Macro.ts: -------------------------------------------------------------------------------- 1 | import {Event} from './Event'; 2 | 3 | interface Arg { 4 | arg: any 5 | def: any 6 | splat: any 7 | } 8 | 9 | interface Macro { 10 | args: Arg[] 11 | steps: Event 12 | } 13 | 14 | type ArgMap = {[arg: string]: Event}; 15 | type NamedArg = { argName: string, argValue: Event }; 16 | type ArgValue = Event | NamedArg; 17 | 18 | export type Macros = {[eventName: string]: Macro}; 19 | 20 | export function expandEvent(macros: Macros, event: Event): Event[] { 21 | const [eventName, ...eventArgs] = event; 22 | 23 | if (macros[eventName]) { 24 | let expanded = expandMacro(macros[eventName], eventArgs); 25 | 26 | // Recursively expand steps 27 | return expanded.map(event => expandEvent(macros, event)).flat(); 28 | } else { 29 | return [event]; 30 | } 31 | } 32 | 33 | function getArgValues(eventArgs: ArgValue[], macroArgs: Arg[]): ArgMap { 34 | const eventArgNameMap: ArgMap = {}; 35 | const eventArgIndexed: Event[] = []; 36 | const argValues: ArgMap = {}; 37 | let usedNamedArg: boolean = false; 38 | let usedSplat: boolean = false; 39 | 40 | eventArgs.forEach((eventArg) => { 41 | if (eventArg.hasOwnProperty('argName')) { 42 | const {argName, argValue} = eventArg; 43 | 44 | eventArgNameMap[argName] = argValue; 45 | usedNamedArg = true; 46 | } else { 47 | if (usedNamedArg) { 48 | throw new Error("Cannot use positional arg after named arg in macro invokation."); 49 | } 50 | 51 | eventArgIndexed.push(eventArg); 52 | } 53 | }); 54 | 55 | macroArgs.forEach(({arg, def, splat}, argIndex) => { 56 | let val; 57 | 58 | if (usedSplat) { 59 | throw new Error("Cannot have arg after splat arg"); 60 | } 61 | 62 | if (eventArgNameMap[arg] !== undefined) { 63 | val = eventArgNameMap[arg]; 64 | } else if (splat) { 65 | val = eventArgIndexed.slice(argIndex); 66 | usedSplat = true; 67 | } else if (eventArgIndexed[argIndex] !== undefined) { 68 | val = eventArgIndexed[argIndex]; 69 | } else if (def !== undefined) { 70 | val = def; 71 | } else { 72 | throw new Error("Macro cannot find arg value for " + arg); 73 | } 74 | argValues[arg] = val; 75 | }); 76 | 77 | return argValues; 78 | } 79 | 80 | export function expandMacro(macro: Macro, event: Event): Event[] { 81 | const argValues = getArgValues(event, macro.args); 82 | 83 | function expandStep(step) { 84 | return step.map((token) => { 85 | if (argValues[token] !== undefined) { 86 | return argValues[token]; 87 | } else { 88 | if (Array.isArray(token)) { 89 | return expandStep(token); 90 | } else { 91 | return token; 92 | } 93 | } 94 | }); 95 | }; 96 | 97 | return macro.steps.map(expandStep); 98 | } 99 | --------------------------------------------------------------------------------