Skip to content

Commit

Permalink
Merge pull request #41 from usmfum/feat/invariants
Browse files Browse the repository at this point in the history
Feat/invariants
  • Loading branch information
alcueca committed Oct 8, 2020
2 parents dce3395 + 2459924 commit f99a902
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 20 deletions.
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/
2 changes: 1 addition & 1 deletion contracts/FUM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "erc20permit/contracts/ERC20Permit.sol";

/**
* @title FUM Token
* @author Alex Roan (@alexroan)
* @author Alberto Cuesta Cañada, Jacob Eliosoff, Alex Roan
*
* @notice This should be owned by the stablecoin.
*/
Expand Down
5 changes: 4 additions & 1 deletion contracts/Proxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import "@openzeppelin/contracts/utils/Address.sol";
import "./IUSM.sol";
import "./external/IWETH9.sol";


/**
* @title USM Frontend Proxy
* @author Alberto Cuesta Cañada, Jacob Eliosoff, Alex Roan
*/
contract Proxy {
enum EthType {ETH, WETH}

Expand Down
4 changes: 2 additions & 2 deletions contracts/USM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import "./oracles/IOracle.sol";

/**
* @title USM Stable Coin
* @author Alex Roan (@alexroan)
* @author Alberto Cuesta Cañada, Jacob Eliosoff, Alex Roan
* @notice Concept by Jacob Eliosoff (@jacob-eliosoff).
*/
contract USM is IUSM, ERC20Permit, Delegable {
Expand Down Expand Up @@ -57,6 +57,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 @@ -66,7 +67,6 @@ contract USM is IUSM, ERC20Permit, Delegable {
returns (uint)
{
// First calculate:
require(fum.totalSupply() > 0, "Fund before minting");
uint usmOut;
uint ethPoolGrowthFactor;
(usmOut, ethPoolGrowthFactor) = usmFromMint(ethIn);
Expand Down
4 changes: 4 additions & 0 deletions contracts/WadMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ pragma solidity ^0.6.7;

import "@openzeppelin/contracts/math/SafeMath.sol";

/**
* @title Fixed point arithmetic library
* @author Alberto Cuesta Cañada, Jacob Eliosoff, Alex Roan
*/
library WadMath {
using SafeMath for uint;

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() > 0); // 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);
}
}
12 changes: 10 additions & 2 deletions migrations/2_deploy_contracts.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
const TestOracle = artifacts.require("TestOracle");
const ChainlinkOracle = artifacts.require("ChainlinkOracle");
const USM = artifacts.require("USM");
const FUM = artifacts.require("USM");
const WETH9 = artifacts.require("USM");
const Proxy = artifacts.require("Proxy");

module.exports = async function(deployer, network) {
// For some reason, this helps `oracle.address`
// not be undefined??
await web3.eth.net.getId();
const deployer = await web3.eth.getAccounts()[0]

const oracleAddresses = {
'ropsten' : '0x30B5068156688f818cEa0874B580206dFe081a03',
Expand All @@ -19,15 +23,19 @@ module.exports = async function(deployer, network) {
}

let oracle
let weth
if (network !== 'ropsten' && network !== 'rinkeby' && network !== 'kovan') {
await deployer.deploy(TestOracle, "25000000000", "8");
oracle = await TestOracle.deployed()

await deployer.deploy(WETH9);
weth = await WETH9.deployed()
}
else {
await deployer.deploy(ChainlinkOracle, oracleAddresses[network], "8")
oracle = await ChainlinkOracle.deployed()
weth = await WETH9.at(wethAddresses[network])
}

await deployer.deploy(USM, oracle.address/*, wethAddresses[network]*/);
const usm = await USM.deployed()
}
}
16 changes: 8 additions & 8 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,44 +21,44 @@ 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())
let debtRatio = (await usm.debtRatio())
debtRatio.toString().should.equal(ZERO.toString())
})
})
Expand Down
Loading

0 comments on commit f99a902

Please sign in to comment.