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 all 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/
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
6 changes: 0 additions & 6 deletions contracts/IUSM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ pragma solidity ^0.6.7;


interface IUSM {
enum Side {Buy, Sell}

event MinFumBuyPriceChanged(uint previous, uint latest);
event MintBurnAdjustmentChanged(uint previous, uint latest);
event FundDefundAdjustmentChanged(uint previous, uint latest);

function mint(address from, address to, uint ethIn) external returns (uint);
function burn(address from, address to, uint usmToBurn) external returns (uint);
function fund(address from, address to, uint ethIn) external returns (uint);
Expand Down
122 changes: 47 additions & 75 deletions contracts/Proxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ 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}

using Address for address payable;
IUSM public usm;
IWETH9 public weth;
Expand All @@ -20,108 +25,75 @@ contract Proxy {
weth.approve(address(usm), uint(-1));
}

// -------------------
// Using Ether
// -------------------

/// @dev The WETH9 contract will send ether to Proxy on `weth.withdraw` using this function.
receive() external payable { }

/// @dev Users use `mint` in Proxy to post ETH to USM (amount = msg.value), which will be converted to Weth here.
/// @dev Users use `mint()` in Proxy to input either WETH or ETH: either one results in passing WETH to `USM.mint()`.
/// @param ethIn Amount of WETH/ETH to use for minting USM.
/// @param minUsmOut Minimum accepted USM for a successful mint.
function mintWithEth(uint minUsmOut)
/// @param inputType Whether the user passes in WETH, or ETH (which is immediately converted to WETH).
function mint(uint ethIn, uint minUsmOut, EthType inputType)
external payable returns (uint)
{
weth.deposit{ value: msg.value }();
uint usmOut = usm.mint(address(this), msg.sender, msg.value);
address payer = (inputType == EthType.ETH ? address(this) : msg.sender);
if (inputType == EthType.ETH) {
require(msg.value == ethIn, "ETH input misspecified");
weth.deposit{ value: msg.value }();
}
uint usmOut = usm.mint(payer, msg.sender, ethIn);
require(usmOut >= minUsmOut, "Limit not reached");
return usmOut;
}

/// @dev Users wishing to withdraw their Weth as ETH from USM should use this function.
/// @dev Users wishing to withdraw their WETH from USM, either as WETH or as ETH, should use this function.
/// Users must have called `controller.addDelegate(Proxy.address)` to authorize Proxy to act in their behalf.
/// @param usmToBurn Amount of USM to burn.
/// @param minEthOut Minimum accepted ETH for a successful burn.
function burnForEth(uint usmToBurn, uint minEthOut)
/// @param minEthOut Minimum accepted WETH/ETH for a successful burn.
/// @param outputType Whether to send the user WETH, or first convert it to ETH.
function burn(uint usmToBurn, uint minEthOut, EthType outputType)
external returns (uint)
{
uint ethOut = usm.burn(msg.sender, address(this), usmToBurn);
address receiver = (outputType == EthType.ETH ? address(this) : msg.sender);
uint ethOut = usm.burn(msg.sender, receiver, usmToBurn);
require(ethOut >= minEthOut, "Limit not reached");
weth.withdraw(ethOut);
msg.sender.sendValue(ethOut);
if (outputType == EthType.ETH) {
weth.withdraw(ethOut);
msg.sender.sendValue(ethOut);
}
return ethOut;
}

/// @notice Funds the pool with ETH, converted to WETH
/// @param minFumOut Minimum accepted FUM for a successful burn.
function fundWithEth(uint minFumOut)
/// @notice Funds the pool either with WETH, or with ETH (then converted to WETH)
/// @param ethIn Amount of WETH/ETH to use for minting FUM.
/// @param minFumOut Minimum accepted FUM for a successful fund.
/// @param inputType Whether the user passes in WETH, or ETH (which is immediately converted to WETH).
function fund(uint ethIn, uint minFumOut, EthType inputType)
external payable returns (uint)
{
weth.deposit{ value: msg.value }();
uint fumOut = usm.fund(address(this), msg.sender, msg.value);
require(fumOut >= minFumOut, "Limit not reached");
return fumOut;
}

/// @notice Defunds the pool by sending FUM out in exchange for equivalent ETH from the pool
/// @param fumToBurn Amount of FUM to burn.
/// @param minEthOut Minimum accepted ETH for a successful defund.
function defundForEth(uint fumToBurn, uint minEthOut)
external returns (uint)
{
uint ethOut = usm.defund(msg.sender, address(this), fumToBurn);
require(ethOut >= minEthOut, "Limit not reached");
weth.withdraw(ethOut);
msg.sender.sendValue(ethOut);
return ethOut;
}

// -------------------
// Using Wrapped Ether
// -------------------

/// @dev Users use `mint` in Proxy to post Weth to USM.
/// @param ethIn Amount of wrapped eth to use for minting USM.
/// @param minUsmOut Minimum accepted USM for a successful mint.
function mint(uint ethIn, uint minUsmOut)
external returns (uint)
{
uint usmOut = usm.mint(msg.sender, msg.sender, ethIn);
require(usmOut >= minUsmOut, "Limit not reached");
return usmOut;
}

/// @dev Users wishing to withdraw their Weth from USM should use this function.
/// Users must have called `controller.addDelegate(Proxy.address)` to authorize Proxy to act in their behalf.
/// @param usmToBurn Amount of USM to burn.
/// @param minEthOut Minimum accepted WETH for a successful burn.
function burn(uint usmToBurn, uint minEthOut)
external returns (uint)
{
uint ethOut = usm.burn(msg.sender, msg.sender, usmToBurn);
require(ethOut >= minEthOut, "Limit not reached");
return ethOut;
}

/// @notice Funds the pool with WETH
/// @param ethIn Amount of wrapped eth to use for minting FUM.
/// @param minFumOut Minimum accepted FUM for a successful burn.
function fund(uint ethIn, uint minFumOut)
external returns (uint)
{
uint fumOut = usm.fund(msg.sender, msg.sender, ethIn);
address payer = (inputType == EthType.ETH ? address(this) : msg.sender);
if (inputType == EthType.ETH) {
require(msg.value == ethIn, "ETH input misspecified");
weth.deposit{ value: msg.value }();
}
uint fumOut = usm.fund(payer, msg.sender, ethIn);
require(fumOut >= minFumOut, "Limit not reached");
return fumOut;
}

/// @notice Defunds the pool by sending FUM out in exchange for equivalent WETH from the pool
/// @notice Defunds the pool by redeeming FUM in exchange for equivalent WETH from the pool (optionally then converted to ETH)
/// @param fumToBurn Amount of FUM to burn.
/// @param minEthOut Minimum accepted WETH for a successful defund.
function defund(uint fumToBurn, uint minEthOut)
/// @param minEthOut Minimum accepted WETH/ETH for a successful defund.
/// @param outputType Whether to send the user WETH, or first convert it to ETH.
function defund(uint fumToBurn, uint minEthOut, EthType outputType)
external returns (uint)
{
uint ethOut = usm.defund(msg.sender, msg.sender, fumToBurn);
address receiver = (outputType == EthType.ETH ? address(this) : msg.sender);
uint ethOut = usm.defund(msg.sender, receiver, fumToBurn);
require(ethOut >= minEthOut, "Limit not reached");
if (outputType == EthType.ETH) {
weth.withdraw(ethOut);
msg.sender.sendValue(ethOut);
}
return ethOut;
}
}
10 changes: 8 additions & 2 deletions contracts/USM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ 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 {
using SafeMath for uint;
using WadMath for uint;

enum Side {Buy, Sell}

event MinFumBuyPriceChanged(uint previous, uint latest);
event MintBurnAdjustmentChanged(uint previous, uint latest);
event FundDefundAdjustmentChanged(uint previous, uint latest);

uint public constant WAD = 10 ** 18;
uint public constant MAX_DEBT_RATIO = WAD * 8 / 10; // 80%
uint public constant MIN_FUM_BUY_PRICE_HALF_LIFE = 24 * 60 * 60; // 1 day
Expand Down Expand Up @@ -51,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 @@ -60,7 +67,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
7 changes: 6 additions & 1 deletion contracts/WadMath.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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 Expand Up @@ -70,7 +75,7 @@ library WadMath {
z = n % 2 != 0 ? x : WAD;

for (n /= 2; n != 0; n /= 2) {
x = wadMul(x, x);
x = wadSquared(x);

if (n % 2 != 0) {
z = wadMul(z, x);
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
}
}
Loading