Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/invariants #41

Merged
merged 19 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,6 @@ typings/

# ignore types
types/

# crytic and echidna
crytic-export/
4 changes: 2 additions & 2 deletions contracts/USM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ contract USM is IUSM, ERC20Permit, Delegable {

/**
* @notice Mint ETH for USM with checks and asset transfers. Uses msg.value as the ETH deposit.
* FUM needs to be funded before USM can be minted.
* @param ethIn Amount of wrapped Ether to use for minting USM.
* @return USM minted
*/
Expand All @@ -60,7 +61,6 @@ contract USM is IUSM, ERC20Permit, Delegable {
returns (uint)
{
// First calculate:
require(fum.totalSupply() > 0, "Fund before minting");
alcueca marked this conversation as resolved.
Show resolved Hide resolved
uint usmOut;
uint ethPoolGrowthFactor;
(usmOut, ethPoolGrowthFactor) = usmFromMint(ethIn);
Expand Down Expand Up @@ -184,7 +184,7 @@ contract USM is IUSM, ERC20Permit, Delegable {
function debtRatio() public view returns (uint) {
uint pool = ethPool();
if (pool == 0) {
return 0;
return MAX_DEBT_RATIO;
}
return totalSupply().wadDiv(ethToUsm(pool));
}
Expand Down
88 changes: 88 additions & 0 deletions contracts/fuzzing/USMFuzzingEthMgmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.6.7;
import "../IUSM.sol";
import "../USM.sol";
import "../FUM.sol";
import "../oracles/TestOracle.sol";
import "../mocks/MockWETH9.sol";
import "../WadMath.sol";
import "@nomiclabs/buidler/console.sol";


/**
* This fuzzing contract tests that USM.sol moves Eth between itself and the clients accordingly to the parameters and return values of mint, burn, fund and defund.
*/
contract USMFuzzingEthMgmt {
using WadMath for uint;

USM internal usm;
FUM internal fum;
MockWETH9 internal weth;
TestOracle internal oracle;

constructor() public {
weth = new MockWETH9();
oracle = new TestOracle(25000000000, 8);
usm = new USM(address(oracle), address(weth));
fum = FUM(usm.fum());

weth.approve(address(usm), uint(-1));
usm.approve(address(usm), uint(-1));
fum.approve(address(usm), uint(-1));
}

/// @dev Test that USM.sol takes eth when minting.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testMintEthValue(uint ethIn) public { // To exclude a function from testing, make it internal
// A failing require aborts this test instance without failing the fuzzing
require(ethIn >= 10**14); // I'm restricting tests to a range of inputs with this

weth.mint(ethIn);

uint valueBefore = weth.balanceOf(address(usm));
usm.mint(address(this), address(this), ethIn);
uint valueAfter = weth.balanceOf(address(usm));

// The asserts are what we are testing. A failing assert will be reported.
assert(valueBefore + ethIn == valueAfter); // The value in eth of the USM supply increased by as much as the eth that went in
}

/// @dev Test that USM.sol returns eth when burning.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testBurnEthValue(uint usmOut) public { // To exclude a function from testing, make it internal
// A failing require aborts this test instance without failing the fuzzing
require(usmOut >= 10**14); // I'm restricting tests to a range of inputs with this

uint valueBefore = weth.balanceOf(address(usm));
uint ethOut = usm.burn(address(this), address(this), usmOut);
uint valueAfter = weth.balanceOf(address(usm));

assert(valueBefore - ethOut == valueAfter); // The value in eth of the USM supply decreased by as much as the value in eth of the USM that was burnt
}

/// @dev Test that USM.sol takes eth when funding.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testFundEthValue(uint ethIn) public { // To exclude a function from testing, make it internal
require(ethIn >= 10**14); // 10**14 + 1 fails the last assertion

weth.mint(ethIn);

uint valueBefore = weth.balanceOf(address(usm));
usm.fund(address(this), address(this), ethIn);
uint valueAfter = weth.balanceOf(address(usm));

assert(valueBefore + ethIn <= valueAfter); // The value in eth of the FUM supply increased by as much as the eth that went in
}

/// @dev Test that USM.sol returns eth when defunding.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testDefundEthValue(uint fumOut) public { // To exclude a function from testing, make it internal
require(fumOut >= 10**14); // 10**14 + 1 fails the last assertion

uint valueBefore = weth.balanceOf(address(usm));
uint ethOut = usm.defund(address(this), address(this), fumOut);
uint valueAfter = weth.balanceOf(address(usm));

assert(valueBefore - ethOut == valueAfter); // The value in eth of the FUM supply decreased by as much as the value in eth of the FUM that was burnt
}
}
56 changes: 56 additions & 0 deletions contracts/fuzzing/USMFuzzingRoundtrip.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.6.7;
import "../IUSM.sol";
import "../USM.sol";
import "../FUM.sol";
import "../oracles/TestOracle.sol";
import "../mocks/MockWETH9.sol";
import "../WadMath.sol";
import "@nomiclabs/buidler/console.sol";

contract USMFuzzingRoundtrip {
using WadMath for uint;

USM internal usm;
FUM internal fum;
MockWETH9 internal weth;
TestOracle internal oracle;

constructor() public {
weth = new MockWETH9();
oracle = new TestOracle(25000000000, 8);
usm = new USM(address(oracle), address(weth));
fum = FUM(usm.fum());

weth.approve(address(usm), uint(-1));
usm.approve(address(usm), uint(-1));
fum.approve(address(usm), uint(-1));
}

/// @dev Test minting USM increases the value of the system by the same amount as Eth provided, and that burning does the inverse.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testMintAndBurnEthValue(uint ethIn) public { // To exclude a function from testing, make it internal
weth.mint(ethIn);

uint usmOut = usm.mint(address(this), address(this), ethIn);
uint ethOut = usm.burn(address(this), address(this), usmOut);

assert(ethIn >= ethOut);
}

/// @dev Test minting USM increases the value of the system by the same amount as Eth provided, and that burning does the inverse.
/// Any function that is public will be run as a test, with random values assigned to each parameter
function testFundAndDefundEthValue(uint ethIn) public { // To exclude a function from testing, make it internal
weth.mint(ethIn);

uint fumOut = usm.fund(address(this), address(this), ethIn);
uint ethOut = usm.defund(address(this), address(this), fumOut);

require(fum.totalSupply() >= 10**18); // TODO: Edge case - Removing all FUM leaves ETH in USM that will be claimed by the next `fund()`

assert(ethIn >= ethOut);
}

// Test that ethBuffer grows up with the fund/defund/mint/burn fee, plus minus eth for fund eth from defund
// Test that with two consecutive ops, the second one gets a worse price
}
21 changes: 21 additions & 0 deletions contracts/fuzzing/WETH9Fuzzing.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.6.7;
import "../mocks/MockWETH9.sol";
import "@nomiclabs/buidler/console.sol";

contract WETH9Fuzzing {

MockWETH9 internal weth;

constructor () public {
weth = new MockWETH9();
}

function fuzzMint(uint ethAmount) public {
uint supply = weth.totalSupply();
uint balance = weth.balanceOf(address(this));
weth.mint(ethAmount);
assert(weth.totalSupply() == supply); // Since `mint` is a hack, t doesn't change the supply
assert(weth.balanceOf(address(this)) == balance + ethAmount);
}
}
8 changes: 8 additions & 0 deletions contracts/fuzzing/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
seqLen: 50
testLimit: 20000
prefix: "crytic_"
deployer: "0x41414141"
sender: ["0x42424242", "0x43434343"]
cryticArgs: ["--compile-force-framework", "Buidler"]
coverage: true
checkAsserts: true
13 changes: 13 additions & 0 deletions contracts/mocks/MockWETH9.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.6.7;

import "../external/WETH9.sol";


contract MockWETH9 is WETH9 {

function mint(uint amount) public {
balanceOf[msg.sender] += amount;
emit Deposit(msg.sender, amount);
}
}
22 changes: 11 additions & 11 deletions test/02_USM_internal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require('chai').use(require('chai-as-promised')).should()

contract('USM - Internal functions', (accounts) => {
const [deployer, user1, user2, user3] = accounts
let mockToken
let usm

const price = new BN('25000000000')
const shift = new BN('8')
Expand All @@ -21,45 +21,45 @@ contract('USM - Internal functions', (accounts) => {
beforeEach(async () => {
oracle = await TestOracle.new(price, shift, { from: deployer })
weth = await WETH9.new({ from: deployer })
mockToken = await MockUSM.new(oracle.address, weth.address, { from: deployer })
usm = await MockUSM.new(oracle.address, weth.address, { from: deployer })
})

describe('deployment', async () => {
it('returns the correct price', async () => {
let oraclePrice = (await oracle.latestPrice())
let oraclePrice = await oracle.latestPrice()
oraclePrice.toString().should.equal(price.toString())
})

it('returns the correct decimal shift', async () => {
let decimalshift = (await oracle.decimalShift())
let decimalshift = await oracle.decimalShift()
decimalshift.toString().should.equal(shift.toString())
})
})

describe('functionality', async () => {
it('returns the oracle price in WAD', async () => {
let oraclePrice = (await mockToken.oraclePrice())
let oraclePrice = await usm.oraclePrice()
oraclePrice.toString().should.equal(priceWAD.toString())
})

it('returns the value of eth in usm', async () => {
const oneEth = WAD
const equivalentUSM = oneEth.mul(priceWAD).div(WAD)
let usmAmount = (await mockToken.ethToUsm(oneEth))
let usmAmount = await usm.ethToUsm(oneEth)
usmAmount.toString().should.equal(equivalentUSM.toString())
})

it('returns the value of usm in eth', async () => {
const oneUSM = WAD
const equivalentEth = oneUSM.mul(WAD).div(priceWAD)
let ethAmount = (await mockToken.usmToEth(oneUSM.toString()))
let ethAmount = await usm.usmToEth(oneUSM.toString())
ethAmount.toString().should.equal(equivalentEth.toString())
})

it('returns the debt ratio as zero', async () => {
const ZERO = new BN('0')
let debtRatio = (await mockToken.debtRatio())
debtRatio.toString().should.equal(ZERO.toString())
it('returns the debt ratio as MAX_DEBT_RATIO', async () => {
const MAX_DEBT_RATIO = await usm.MAX_DEBT_RATIO()
alcueca marked this conversation as resolved.
Show resolved Hide resolved
let debtRatio = (await usm.debtRatio())
debtRatio.toString().should.equal(MAX_DEBT_RATIO.toString())
})
})
})
5 changes: 4 additions & 1 deletion test/03_USM.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ contract('USM', (accounts) => {

describe("minting and burning", () => {
it("doesn't allow minting USM before minting FUM", async () => {
await expectRevert(usm.mint(user2, user1, oneEth, { from: user2 }), "Fund before minting")
await expectRevert(
usm.mint(user1, user1, oneEth, { from: user1 }),
"SafeMath: division by zero"
)
})

it("allows minting FUM", async () => {
Expand Down
92 changes: 92 additions & 0 deletions test/fuzzing/11_USM_Fuzzing.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const { BN, expectRevert } = require('@openzeppelin/test-helpers')
const timeMachine = require('ganache-time-traveler')

const TestOracle = artifacts.require('./TestOracle.sol')
const WETH9 = artifacts.require('WETH9')
const USM = artifacts.require('./USM.sol')
const FUM = artifacts.require('./FUM.sol')

require('chai').use(require('chai-as-promised')).should()

contract('USM', (accounts) => {
function wadMul(x, y) {
return x.mul(y).div(WAD);
}

function wadSquared(x) {
return x.mul(x).div(WAD);
}

function wadDiv(x, y) {
return x.mul(WAD).div(y);
}

function wadDecay(adjustment, decayFactor) {
return WAD.add(wadMul(adjustment, decayFactor)).sub(decayFactor)
}

function shouldEqual(x, y) {
x.toString().should.equal(y.toString())
}

function shouldEqualApprox(x, y, precision) {
x.sub(y).abs().should.be.bignumber.lte(precision)
}

const [deployer, user1, user2, user3] = accounts

const [ONE, TWO, THREE, FOUR, EIGHT, TEN, HUNDRED, THOUSAND] =
[1, 2, 3, 4, 8, 10, 100, 1000].map(function (n) { return new BN(n) })
const WAD = new BN('1000000000000000000')

const sides = { BUY: 0, SELL: 1 }
const price = new BN('25000000000')
const shift = EIGHT
const oneEth = WAD
const oneUsm = WAD
const oneFum = WAD
const MINUTE = 60
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
const priceWAD = wadDiv(price, TEN.pow(shift))

describe("Fuzzing debugging helper", () => {
let oracle, weth, usm

beforeEach(async () => {
// Deploy contracts
oracle = await TestOracle.new(price, shift, { from: deployer })
weth = await WETH9.new({ from: deployer })
usm = await USM.new(oracle.address, weth.address, { from: deployer })
fum = await FUM.at(await usm.fum())

let snapshot = await timeMachine.takeSnapshot()
snapshotId = snapshot['result']
})

afterEach(async () => {
await timeMachine.revertToSnapshot(snapshotId)
})

describe("minting and burning", () => {
it("fund and defund round trip", async () => {
const ethIns = ['200790178637337', '100000000000001']
for (let ethIn of ethIns) {
console.log('')
console.log(` > ethIn: ${ethIn}`)

await weth.deposit({ from: user1, value: ethIn })
await weth.approve(usm.address, ethIn, { from: user1 })

const fumOut = await usm.fund.call(user1, user1, ethIn, { from: user1 })
await usm.fund(user1, user1, ethIn, { from: user1 })
console.log(` > fumOut: ${fumOut}`)

const ethOut = await usm.defund.call(user1, user1, fumOut, { from: user1 })
await usm.defund(user1, user1, fumOut, { from: user1 })
console.log(` > ethOut: ${ethOut}`)
}
})
})
})
})