From fb90ed7b501722af1f0adc8b7f909ef58ce3bac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Fri, 2 Oct 2020 13:55:57 +0100 Subject: [PATCH 01/13] feat: invariant compiles --- .gitignore | 3 + contracts/fuzzing/ReversalInvariant.sol | 75 +++++++++++++++++++++++++ contracts/fuzzing/config.yaml | 8 +++ contracts/mocks/MockWETH9.sol | 13 +++++ 4 files changed, 99 insertions(+) create mode 100644 contracts/fuzzing/ReversalInvariant.sol create mode 100644 contracts/fuzzing/config.yaml create mode 100644 contracts/mocks/MockWETH9.sol diff --git a/.gitignore b/.gitignore index f0e1170..2a98ade 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ typings/ # ignore types types/ + +# crytic and echidna +crytic-export/ \ No newline at end of file diff --git a/contracts/fuzzing/ReversalInvariant.sol b/contracts/fuzzing/ReversalInvariant.sol new file mode 100644 index 0000000..7171cdf --- /dev/null +++ b/contracts/fuzzing/ReversalInvariant.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.6.7; +import "../USM.sol"; +import "../oracles/TestOracle.sol"; +import "../mocks/MockWETH9.sol"; +import "@nomiclabs/buidler/console.sol"; + +contract ReversalInvariant is USM { + + constructor() public USM(address(new TestOracle(25000000000, 8)), address(new MockWETH9())) { } + + /// @dev Test that burning usm renders less than the eth that was used for minting it. When the transactions happen atomically. + function testMintAndBurn( + uint fumSupply, + uint usmSupply, + uint ethBalance, + uint oraclePrice, + uint ethIn, + uint32 fundDefundAdjustmentStoredTimestamp, + uint224 fundDefundAdjustmentStoredValue, + uint32 mintBurnAdjustmentStoredTimestamp, + uint224 mintBurnAdjustmentStoredValue + ) public { + // Set parameters within range + oraclePrice %= 1e14; + fundDefundAdjustmentStoredTimestamp %= uint32(block.timestamp); + // fundDefundAdjustmentStoredValue <- Reasonable range for this? + mintBurnAdjustmentStoredTimestamp %= uint32(block.timestamp); + // mintBurnAdjustmentStoredValue <- Reasonable range for this? + + // Set contract state + fum.mint(address(this), fumSupply); + _mint(address(this), usmSupply); + MockWETH9(payable(address(eth))).mint(ethBalance); + TestOracle(address(oracle)).setPrice(oraclePrice); + fundDefundAdjustmentStored = TimedValue({ + timestamp: fundDefundAdjustmentStoredTimestamp, + value: fundDefundAdjustmentStoredValue + }); + mintBurnAdjustmentStored = TimedValue({ + timestamp: mintBurnAdjustmentStoredTimestamp, + value: mintBurnAdjustmentStoredValue + }); + + // Transactions + uint usmOut = this.mint(address(this), address(this), ethIn); + uint ethOut = this.burn(address(this), address(this), usmOut); + + // This is what we are testing + assert(ethOut < ethIn); + + // Parameters influencing mint + // fum.totalSupply() + // oracle price + // eth.balanceOf(this) + // ethIn <- Input + // block.timestamp + // fundDefundAdjustmentStored.timestamp + // fundDefundAdjustmentStored.value + // mintBurnAdjustmentStored.timestamp + // mintBurnAdjustmentStored.value + // -> usmOut + + // Parameters influencing burn + // oraclePrice + // eth.balanceOf(this) + // usmOut <- Input + // usm.totalSupply() + // block.timestamp + // fundDefundAdjustmentStored.timestamp + // fundDefundAdjustmentStored.value + // mintBurnAdjustmentStored.timestamp + // mintBurnAdjustmentStored.value + } +} \ No newline at end of file diff --git a/contracts/fuzzing/config.yaml b/contracts/fuzzing/config.yaml new file mode 100644 index 0000000..fc4f912 --- /dev/null +++ b/contracts/fuzzing/config.yaml @@ -0,0 +1,8 @@ +seqLen: 50 +testLimit: 20000 +prefix: "crytic_" +deployer: "0x41414141" +sender: ["0x42424242", "0x43434343"] +cryticArgs: ["--compile-force-framework", "Buidler"] +coverage: true +checkAsserts: true \ No newline at end of file diff --git a/contracts/mocks/MockWETH9.sol b/contracts/mocks/MockWETH9.sol new file mode 100644 index 0000000..2cacf5c --- /dev/null +++ b/contracts/mocks/MockWETH9.sol @@ -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); + } +} \ No newline at end of file From faeb01f314c90d65dbb0d5407a4c56f2a24d1d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Sun, 4 Oct 2020 21:07:42 +0100 Subject: [PATCH 02/13] feat: First USM fuzzing tests --- contracts/fuzzing/ReversalInvariant.sol | 75 ------------------------- contracts/fuzzing/USMFuzzing.sol | 63 +++++++++++++++++++++ contracts/fuzzing/WETH9Fuzzing.sol | 21 +++++++ 3 files changed, 84 insertions(+), 75 deletions(-) delete mode 100644 contracts/fuzzing/ReversalInvariant.sol create mode 100644 contracts/fuzzing/USMFuzzing.sol create mode 100644 contracts/fuzzing/WETH9Fuzzing.sol diff --git a/contracts/fuzzing/ReversalInvariant.sol b/contracts/fuzzing/ReversalInvariant.sol deleted file mode 100644 index 7171cdf..0000000 --- a/contracts/fuzzing/ReversalInvariant.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.6.7; -import "../USM.sol"; -import "../oracles/TestOracle.sol"; -import "../mocks/MockWETH9.sol"; -import "@nomiclabs/buidler/console.sol"; - -contract ReversalInvariant is USM { - - constructor() public USM(address(new TestOracle(25000000000, 8)), address(new MockWETH9())) { } - - /// @dev Test that burning usm renders less than the eth that was used for minting it. When the transactions happen atomically. - function testMintAndBurn( - uint fumSupply, - uint usmSupply, - uint ethBalance, - uint oraclePrice, - uint ethIn, - uint32 fundDefundAdjustmentStoredTimestamp, - uint224 fundDefundAdjustmentStoredValue, - uint32 mintBurnAdjustmentStoredTimestamp, - uint224 mintBurnAdjustmentStoredValue - ) public { - // Set parameters within range - oraclePrice %= 1e14; - fundDefundAdjustmentStoredTimestamp %= uint32(block.timestamp); - // fundDefundAdjustmentStoredValue <- Reasonable range for this? - mintBurnAdjustmentStoredTimestamp %= uint32(block.timestamp); - // mintBurnAdjustmentStoredValue <- Reasonable range for this? - - // Set contract state - fum.mint(address(this), fumSupply); - _mint(address(this), usmSupply); - MockWETH9(payable(address(eth))).mint(ethBalance); - TestOracle(address(oracle)).setPrice(oraclePrice); - fundDefundAdjustmentStored = TimedValue({ - timestamp: fundDefundAdjustmentStoredTimestamp, - value: fundDefundAdjustmentStoredValue - }); - mintBurnAdjustmentStored = TimedValue({ - timestamp: mintBurnAdjustmentStoredTimestamp, - value: mintBurnAdjustmentStoredValue - }); - - // Transactions - uint usmOut = this.mint(address(this), address(this), ethIn); - uint ethOut = this.burn(address(this), address(this), usmOut); - - // This is what we are testing - assert(ethOut < ethIn); - - // Parameters influencing mint - // fum.totalSupply() - // oracle price - // eth.balanceOf(this) - // ethIn <- Input - // block.timestamp - // fundDefundAdjustmentStored.timestamp - // fundDefundAdjustmentStored.value - // mintBurnAdjustmentStored.timestamp - // mintBurnAdjustmentStored.value - // -> usmOut - - // Parameters influencing burn - // oraclePrice - // eth.balanceOf(this) - // usmOut <- Input - // usm.totalSupply() - // block.timestamp - // fundDefundAdjustmentStored.timestamp - // fundDefundAdjustmentStored.value - // mintBurnAdjustmentStored.timestamp - // mintBurnAdjustmentStored.value - } -} \ No newline at end of file diff --git a/contracts/fuzzing/USMFuzzing.sol b/contracts/fuzzing/USMFuzzing.sol new file mode 100644 index 0000000..c13718f --- /dev/null +++ b/contracts/fuzzing/USMFuzzing.sol @@ -0,0 +1,63 @@ +// 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 USMFuzzing { + 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 valueBefore = usm.usmToEth(usm.totalSupply()); + uint usmOut = usm.mint(address(this), address(this), ethIn); + uint valueMiddle = usm.usmToEth(usm.totalSupply()); + + // assert(valueBefore + ethIn == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in + + uint ethOut = usm.burn(address(this), address(this), usmOut); + uint valueAfter = usm.usmToEth(usm.totalSupply()); + + assert(ethOut == ethIn); // Minting and then burning USM is neutral in eth terms in absence of oracle changes + // assert(valueBefore == 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 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 valueBefore = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + uint fumOut = usm.fund(address(this), address(this), ethIn); + uint valueMiddle = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + + // assert(valueBefore + ethIn == valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in + + uint ethOut = usm.defund(address(this), address(this), fumOut); + uint valueAfter = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + + assert(ethOut <= ethIn); // Minting and then burning FUM is neutral or losses value in eth terms in absence of oracle changes + // assert(valueBefore == valueAfter); // The value in eth of the FUM supply decreased by as much as the value in eth of the FUM that was burnt + } +} \ No newline at end of file diff --git a/contracts/fuzzing/WETH9Fuzzing.sol b/contracts/fuzzing/WETH9Fuzzing.sol new file mode 100644 index 0000000..9b0545a --- /dev/null +++ b/contracts/fuzzing/WETH9Fuzzing.sol @@ -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); + } +} \ No newline at end of file From da3d306b6f9d3aa15312c46cd473e5d8fff3354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Sun, 4 Oct 2020 21:55:56 +0100 Subject: [PATCH 03/13] feat: tests refined with FUM pricing --- contracts/fuzzing/USMFuzzing.sol | 39 ++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/contracts/fuzzing/USMFuzzing.sol b/contracts/fuzzing/USMFuzzing.sol index c13718f..19b7a07 100644 --- a/contracts/fuzzing/USMFuzzing.sol +++ b/contracts/fuzzing/USMFuzzing.sol @@ -31,33 +31,48 @@ contract USMFuzzing { /// 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 valueBefore = usm.usmToEth(usm.totalSupply()); + int valueBefore = usmValue() + fumValue(); uint usmOut = usm.mint(address(this), address(this), ethIn); - uint valueMiddle = usm.usmToEth(usm.totalSupply()); + int valueMiddle = usmValue() + fumValue(); - // assert(valueBefore + ethIn == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in + assert(valueBefore + toInt(ethIn) == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in uint ethOut = usm.burn(address(this), address(this), usmOut); - uint valueAfter = usm.usmToEth(usm.totalSupply()); + int valueAfter = usmValue() + fumValue(); - assert(ethOut == ethIn); // Minting and then burning USM is neutral in eth terms in absence of oracle changes - // assert(valueBefore == valueAfter); // The value in eth of the USM supply decreased by as much as the value in eth of the USM that was burnt + assert(ethOut <= ethIn); // Minting and then burning USM should never produce an Eth profit + assert(valueBefore == 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 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 valueBefore = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + int valueBefore = fumValue(); uint fumOut = usm.fund(address(this), address(this), ethIn); - uint valueMiddle = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + int valueMiddle = fumValue(); - // assert(valueBefore + ethIn == valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in + assert(valueBefore + toInt(ethIn) == valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in uint ethOut = usm.defund(address(this), address(this), fumOut); - uint valueAfter = usm.fumPrice(IUSM.Side.Buy).wadMul(fum.totalSupply()); + int valueAfter = fumValue(); - assert(ethOut <= ethIn); // Minting and then burning FUM is neutral or losses value in eth terms in absence of oracle changes - // assert(valueBefore == valueAfter); // The value in eth of the FUM supply decreased by as much as the value in eth of the FUM that was burnt + assert(ethOut <= ethIn); // Funding and then defunding FUM should never produce an Eth profit, despite fee distribution + assert(valueBefore == valueAfter); // The value in eth of the FUM supply decreased by as much as the value in eth of the FUM that was burnt + } + + function fumValue() internal view returns(int) { + int b = usm.ethBuffer(); + uint s = fum.totalSupply(); + return (b > 0 && s > 0) ? b / toInt(s) : 0; + } + + function usmValue() internal view returns(int) { + return toInt(usm.usmToEth(usm.totalSupply())); + } + + function toInt(uint x) internal pure returns(int) { + require(x < (type(uint).max)/2); + return int(x); } } \ No newline at end of file From 4b68fffddd928b88c54c166dc6f1b33080fe21a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Mon, 5 Oct 2020 09:11:02 +0100 Subject: [PATCH 04/13] feat: Protocol value testing --- contracts/fuzzing/USMFuzzing.sol | 48 +++++++++++++++----------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/contracts/fuzzing/USMFuzzing.sol b/contracts/fuzzing/USMFuzzing.sol index 19b7a07..ca51306 100644 --- a/contracts/fuzzing/USMFuzzing.sol +++ b/contracts/fuzzing/USMFuzzing.sol @@ -30,49 +30,45 @@ contract USMFuzzing { /// @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 + // 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); - int valueBefore = usmValue() + fumValue(); + + uint valueBefore = weth.balanceOf(address(usm)); uint usmOut = usm.mint(address(this), address(this), ethIn); - int valueMiddle = usmValue() + fumValue(); + uint valueMiddle = weth.balanceOf(address(usm)); - assert(valueBefore + toInt(ethIn) == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in + // The asserts are what we are testing. A failing assert will be reported. + assert(valueBefore + ethIn == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in uint ethOut = usm.burn(address(this), address(this), usmOut); - int valueAfter = usmValue() + fumValue(); + uint valueAfter = weth.balanceOf(address(usm)); - assert(ethOut <= ethIn); // Minting and then burning USM should never produce an Eth profit - assert(valueBefore == valueAfter); // The value in eth of the USM supply decreased by as much as the value in eth of the USM that was burnt + assert(valueMiddle - 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 + assert(valueAfter >= valueBefore); // The protocol shouldn't have lost value with the round trip } /// @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 + require(ethIn >= 10**14); // 10**14 + 1 fails the last assertion + weth.mint(ethIn); - int valueBefore = fumValue(); + + uint valueBefore = weth.balanceOf(address(usm)); uint fumOut = usm.fund(address(this), address(this), ethIn); - int valueMiddle = fumValue(); + uint valueMiddle = weth.balanceOf(address(usm)); - assert(valueBefore + toInt(ethIn) == valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in + assert(valueBefore + ethIn <= valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in uint ethOut = usm.defund(address(this), address(this), fumOut); - int valueAfter = fumValue(); + uint valueAfter = weth.balanceOf(address(usm)); - assert(ethOut <= ethIn); // Funding and then defunding FUM should never produce an Eth profit, despite fee distribution - assert(valueBefore == valueAfter); // The value in eth of the FUM supply decreased by as much as the value in eth of the FUM that was burnt + assert(valueMiddle - 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 + assert(valueAfter >= valueBefore); // The protocol shouldn't have lost value with the round trip } - function fumValue() internal view returns(int) { - int b = usm.ethBuffer(); - uint s = fum.totalSupply(); - return (b > 0 && s > 0) ? b / toInt(s) : 0; - } - - function usmValue() internal view returns(int) { - return toInt(usm.usmToEth(usm.totalSupply())); - } - - function toInt(uint x) internal pure returns(int) { - require(x < (type(uint).max)/2); - return int(x); - } + // 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 } \ No newline at end of file From daad5919271d621fe5c1471aaabe530159222d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Mon, 5 Oct 2020 21:45:01 +0100 Subject: [PATCH 05/13] test: Added fund and defund fuzzing debugger --- test/fuzzing/11_USM_Fuzzing.test.js | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/fuzzing/11_USM_Fuzzing.test.js diff --git a/test/fuzzing/11_USM_Fuzzing.test.js b/test/fuzzing/11_USM_Fuzzing.test.js new file mode 100644 index 0000000..2c7748d --- /dev/null +++ b/test/fuzzing/11_USM_Fuzzing.test.js @@ -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}`) + } + }) + }) + }) +}) \ No newline at end of file From 31c942cd7e6b752de7562f18b041b0df2c312939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Wed, 7 Oct 2020 09:47:51 +0100 Subject: [PATCH 06/13] fuzz: Isolated edge case --- contracts/fuzzing/USMFuzzing.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/fuzzing/USMFuzzing.sol b/contracts/fuzzing/USMFuzzing.sol index ca51306..07ef02f 100644 --- a/contracts/fuzzing/USMFuzzing.sol +++ b/contracts/fuzzing/USMFuzzing.sol @@ -65,6 +65,8 @@ contract USMFuzzing { uint ethOut = usm.defund(address(this), address(this), fumOut); uint valueAfter = weth.balanceOf(address(usm)); + require(fum.totalSupply() >= 10**18); // TODO: Edge case - Removing all FUM leaves ETH in USM that will be claimed by the next `fund()` + assert(valueMiddle - 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 assert(valueAfter >= valueBefore); // The protocol shouldn't have lost value with the round trip } From 157e7ae33395479d05b5b0f04eb25d8251ef15c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Wed, 7 Oct 2020 10:27:52 +0100 Subject: [PATCH 07/13] fuzz: Split and simplified tests --- contracts/fuzzing/USMFuzzingEthMgmt.sol | 88 +++++++++++++++++++ ...USMFuzzing.sol => USMFuzzingRoundtrip.sol} | 26 +----- 2 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 contracts/fuzzing/USMFuzzingEthMgmt.sol rename contracts/fuzzing/{USMFuzzing.sol => USMFuzzingRoundtrip.sol} (58%) diff --git a/contracts/fuzzing/USMFuzzingEthMgmt.sol b/contracts/fuzzing/USMFuzzingEthMgmt.sol new file mode 100644 index 0000000..7954b61 --- /dev/null +++ b/contracts/fuzzing/USMFuzzingEthMgmt.sol @@ -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 + } +} \ No newline at end of file diff --git a/contracts/fuzzing/USMFuzzing.sol b/contracts/fuzzing/USMFuzzingRoundtrip.sol similarity index 58% rename from contracts/fuzzing/USMFuzzing.sol rename to contracts/fuzzing/USMFuzzingRoundtrip.sol index 07ef02f..a90a78c 100644 --- a/contracts/fuzzing/USMFuzzing.sol +++ b/contracts/fuzzing/USMFuzzingRoundtrip.sol @@ -8,7 +8,7 @@ import "../mocks/MockWETH9.sol"; import "../WadMath.sol"; import "@nomiclabs/buidler/console.sol"; -contract USMFuzzing { +contract USMFuzzingRoundtrip { using WadMath for uint; USM internal usm; @@ -30,45 +30,25 @@ contract USMFuzzing { /// @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 - // 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)); uint usmOut = usm.mint(address(this), address(this), ethIn); - uint valueMiddle = weth.balanceOf(address(usm)); - - // The asserts are what we are testing. A failing assert will be reported. - assert(valueBefore + ethIn == valueMiddle); // The value in eth of the USM supply increased by as much as the eth that went in - uint ethOut = usm.burn(address(this), address(this), usmOut); - uint valueAfter = weth.balanceOf(address(usm)); - assert(valueMiddle - 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 - assert(valueAfter >= valueBefore); // The protocol shouldn't have lost value with the round trip + 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 - require(ethIn >= 10**14); // 10**14 + 1 fails the last assertion - weth.mint(ethIn); - uint valueBefore = weth.balanceOf(address(usm)); uint fumOut = usm.fund(address(this), address(this), ethIn); - uint valueMiddle = weth.balanceOf(address(usm)); - - assert(valueBefore + ethIn <= valueMiddle); // The value in eth of the FUM supply increased by as much as the eth that went in - uint ethOut = usm.defund(address(this), address(this), fumOut); - uint valueAfter = weth.balanceOf(address(usm)); require(fum.totalSupply() >= 10**18); // TODO: Edge case - Removing all FUM leaves ETH in USM that will be claimed by the next `fund()` - assert(valueMiddle - 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 - assert(valueAfter >= valueBefore); // The protocol shouldn't have lost value with the round trip + assert(ethIn >= ethOut); } // Test that ethBuffer grows up with the fund/defund/mint/burn fee, plus minus eth for fund eth from defund From 7f69c61ae3d93b467180f6bfc18551046a20f47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Wed, 7 Oct 2020 15:57:59 +0100 Subject: [PATCH 08/13] fix: debtRatio when pool is zero and remove require --- contracts/USM.sol | 4 ++-- test/02_USM_internal.test.js | 18 +++++++++--------- test/03_USM.test.js | 5 ++++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/contracts/USM.sol b/contracts/USM.sol index 0ddd5a2..384ff51 100644 --- a/contracts/USM.sol +++ b/contracts/USM.sol @@ -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 */ @@ -60,7 +61,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); @@ -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)); } diff --git a/test/02_USM_internal.test.js b/test/02_USM_internal.test.js index 7505e3e..d4de418 100644 --- a/test/02_USM_internal.test.js +++ b/test/02_USM_internal.test.js @@ -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') @@ -21,7 +21,7 @@ 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 () => { @@ -38,28 +38,28 @@ contract('USM - Internal functions', (accounts) => { 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() + let debtRatio = (await usm.debtRatio()) + debtRatio.toString().should.equal(MAX_DEBT_RATIO.toString()) }) }) }) diff --git a/test/03_USM.test.js b/test/03_USM.test.js index 9b4bfd8..c804050 100644 --- a/test/03_USM.test.js +++ b/test/03_USM.test.js @@ -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 () => { From 47315396d90841e141645bae80f4c5dda96d530d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Wed, 7 Oct 2020 16:02:31 +0100 Subject: [PATCH 09/13] fix: remove parenthesis --- test/02_USM_internal.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/02_USM_internal.test.js b/test/02_USM_internal.test.js index d4de418..2cc51f5 100644 --- a/test/02_USM_internal.test.js +++ b/test/02_USM_internal.test.js @@ -26,33 +26,33 @@ contract('USM - Internal functions', (accounts) => { 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 usm.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 usm.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 usm.usmToEth(oneUSM.toString())) + let ethAmount = await usm.usmToEth(oneUSM.toString()) ethAmount.toString().should.equal(equivalentEth.toString()) }) From e5ccfebb561a7ebb23f476ffa9ec7ef30524c948 Mon Sep 17 00:00:00 2001 From: Jacob Eliosoff Date: Wed, 7 Oct 2020 13:58:26 -0400 Subject: [PATCH 10/13] Actually, keep debtRatio() returning 0, and require FUM supply > 0. --- contracts/USM.sol | 3 ++- test/02_USM_internal.test.js | 6 +++--- test/03_USM.test.js | 15 ++++++++++----- test/fuzzing/11_USM_Fuzzing.test.js | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/USM.sol b/contracts/USM.sol index f61dbb8..d05448b 100644 --- a/contracts/USM.sol +++ b/contracts/USM.sol @@ -147,6 +147,7 @@ contract USM is IUSM, ERC20Permit, Delegable { _updateFundDefundAdjustment(fundDefundAdjustment().wadMul(ethPoolShrinkFactor.wadSquared())); require(eth.transfer(to, ethOut), "ETH transfer fail"); require(debtRatio() <= MAX_DEBT_RATIO, "Max debt ratio breach"); + require(fum.totalSupply() > 0, "Some FUM must be left"); return ethOut; } @@ -190,7 +191,7 @@ contract USM is IUSM, ERC20Permit, Delegable { function debtRatio() public view returns (uint) { uint pool = ethPool(); if (pool == 0) { - return MAX_DEBT_RATIO; + return 0; } return totalSupply().wadDiv(ethToUsm(pool)); } diff --git a/test/02_USM_internal.test.js b/test/02_USM_internal.test.js index 2cc51f5..a84c7ea 100644 --- a/test/02_USM_internal.test.js +++ b/test/02_USM_internal.test.js @@ -56,10 +56,10 @@ contract('USM - Internal functions', (accounts) => { ethAmount.toString().should.equal(equivalentEth.toString()) }) - it('returns the debt ratio as MAX_DEBT_RATIO', async () => { - const MAX_DEBT_RATIO = await usm.MAX_DEBT_RATIO() + it('returns the debt ratio as zero', async () => { + const ZERO = new BN('0') let debtRatio = (await usm.debtRatio()) - debtRatio.toString().should.equal(MAX_DEBT_RATIO.toString()) + debtRatio.toString().should.equal(ZERO.toString()) }) }) }) diff --git a/test/03_USM.test.js b/test/03_USM.test.js index 50d726b..f449116 100644 --- a/test/03_USM.test.js +++ b/test/03_USM.test.js @@ -78,7 +78,7 @@ contract('USM', (accounts) => { }) describe("deployment", () => { - it("starts with correct fum price", async () => { + it("starts with correct FUM price", async () => { const fumBuyPrice = (await usm.fumPrice(sides.BUY)) // The FUM price should start off equal to $1, in ETH terms = 1 / price: const targetFumPrice = wadDiv(WAD, priceWAD) @@ -154,20 +154,25 @@ contract('USM', (accounts) => { await usm.fund(user1, user2, totalEthToFund, { from: user1 }) }) - it("reverts fum transfers to the usm contract", async () => { + it("reverts FUM transfers to the USM contract", async () => { await expectRevert( fum.transfer(usm.address, 1), "Don't transfer here" ) }) - it("reverts fum transfers to the fum contract", async () => { + it("reverts FUM transfers to the FUM contract", async () => { await expectRevert( fum.transfer(fum.address, 1), "Don't transfer here" ) }) + it("doesn't allow burning all FUM", async () => { + const allFum = (await fum.totalSupply()) + await expectRevert(usm.defund(user2, user1, allFum, { from: user2 }), "Some FUM must be left") + }) + it("decays fundDefundAdjustment over time", async () => { // Check that fundDefundAdjustment decays properly (well, approximately) over time: // - Our adjustment was previously 4: see targetFundDefundAdj3 above. @@ -244,14 +249,14 @@ contract('USM', (accounts) => { price0 = (await oracle.latestPrice()) }) - it("reverts usm transfers to the usm contract", async () => { + it("reverts USM transfers to the USM contract", async () => { await expectRevert( usm.transfer(usm.address, 1), "Don't transfer here" ) }) - it("reverts usm transfers to the fum contract", async () => { + it("reverts USM transfers to the FUM contract", async () => { await expectRevert( usm.transfer(fum.address, 1), "Don't transfer here" diff --git a/test/fuzzing/11_USM_Fuzzing.test.js b/test/fuzzing/11_USM_Fuzzing.test.js index 2c7748d..2121324 100644 --- a/test/fuzzing/11_USM_Fuzzing.test.js +++ b/test/fuzzing/11_USM_Fuzzing.test.js @@ -89,4 +89,4 @@ contract('USM', (accounts) => { }) }) }) -}) \ No newline at end of file +}) From 806edb9d3dc0237d5dbe3a61f6140be991425fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Wed, 7 Oct 2020 21:35:55 +0100 Subject: [PATCH 11/13] fix: Zero FUM edge case --- contracts/FUM.sol | 2 +- contracts/Proxy.sol | 5 ++++- contracts/USM.sol | 3 +-- contracts/WadMath.sol | 4 ++++ contracts/fuzzing/USMFuzzingRoundtrip.sol | 2 +- migrations/2_deploy_contracts.js | 20 +++++++++++++++++++- test/03_USM.test.js | 12 ------------ 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/contracts/FUM.sol b/contracts/FUM.sol index af45531..3670da4 100644 --- a/contracts/FUM.sol +++ b/contracts/FUM.sol @@ -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. */ diff --git a/contracts/Proxy.sol b/contracts/Proxy.sol index a00a78d..91afb10 100644 --- a/contracts/Proxy.sol +++ b/contracts/Proxy.sol @@ -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} diff --git a/contracts/USM.sol b/contracts/USM.sol index d05448b..bd5ca6d 100644 --- a/contracts/USM.sol +++ b/contracts/USM.sol @@ -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 { @@ -147,7 +147,6 @@ contract USM is IUSM, ERC20Permit, Delegable { _updateFundDefundAdjustment(fundDefundAdjustment().wadMul(ethPoolShrinkFactor.wadSquared())); require(eth.transfer(to, ethOut), "ETH transfer fail"); require(debtRatio() <= MAX_DEBT_RATIO, "Max debt ratio breach"); - require(fum.totalSupply() > 0, "Some FUM must be left"); return ethOut; } diff --git a/contracts/WadMath.sol b/contracts/WadMath.sol index cd96276..68dac6a 100644 --- a/contracts/WadMath.sol +++ b/contracts/WadMath.sol @@ -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; diff --git a/contracts/fuzzing/USMFuzzingRoundtrip.sol b/contracts/fuzzing/USMFuzzingRoundtrip.sol index a90a78c..82f682e 100644 --- a/contracts/fuzzing/USMFuzzingRoundtrip.sol +++ b/contracts/fuzzing/USMFuzzingRoundtrip.sol @@ -46,7 +46,7 @@ contract USMFuzzingRoundtrip { 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()` + require(fum.totalSupply() > 0); // Edge case - Removing all FUM leaves ETH in USM that will be claimed by the next `fund()` assert(ethIn >= ethOut); } diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index 73e195d..e1de223 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -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', @@ -19,15 +23,29 @@ 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]*/); + await deployer.deploy(USM, oracle.address, weth.address); const usm = await USM.deployed() + const fum = await FUM.at(await usm.fum()) + + await deployer.deploy(Proxy, usm.address, weth.address) + const proxy = await Proxy.deployed() + + await weth.deposit(deployer, 1) + await weth.approve(usm.address, 1) + await usm.fund(deployer, deployer, 1) + await fum.transfer('0x0000000000000000000000000000000000000001', 1) } \ No newline at end of file diff --git a/test/03_USM.test.js b/test/03_USM.test.js index f449116..b84f2d8 100644 --- a/test/03_USM.test.js +++ b/test/03_USM.test.js @@ -90,13 +90,6 @@ contract('USM', (accounts) => { }) describe("minting and burning", () => { - it("doesn't allow minting USM before minting FUM", async () => { - await expectRevert( - usm.mint(user1, user1, oneEth, { from: user1 }), - "SafeMath: division by zero" - ) - }) - it("allows minting FUM", async () => { const fumBuyPrice = (await usm.fumPrice(sides.BUY)) const fumSellPrice = (await usm.fumPrice(sides.SELL)) @@ -168,11 +161,6 @@ contract('USM', (accounts) => { ) }) - it("doesn't allow burning all FUM", async () => { - const allFum = (await fum.totalSupply()) - await expectRevert(usm.defund(user2, user1, allFum, { from: user2 }), "Some FUM must be left") - }) - it("decays fundDefundAdjustment over time", async () => { // Check that fundDefundAdjustment decays properly (well, approximately) over time: // - Our adjustment was previously 4: see targetFundDefundAdj3 above. From c52c64c4ec0d6af774b45fee7a497b76ef80b833 Mon Sep 17 00:00:00 2001 From: Jacob Eliosoff Date: Wed, 7 Oct 2020 18:23:41 -0400 Subject: [PATCH 12/13] Don't mint 1 wei; just allow withdrawing the last FUM --- migrations/2_deploy_contracts.js | 14 ++------------ test/03_USM.test.js | 4 ++++ test/fuzzing/11_USM_Fuzzing.test.js | 7 ++++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index e1de223..4fa14ce 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -37,15 +37,5 @@ module.exports = async function(deployer, network) { weth = await WETH9.at(wethAddresses[network]) } - await deployer.deploy(USM, oracle.address, weth.address); - const usm = await USM.deployed() - const fum = await FUM.at(await usm.fum()) - - await deployer.deploy(Proxy, usm.address, weth.address) - const proxy = await Proxy.deployed() - - await weth.deposit(deployer, 1) - await weth.approve(usm.address, 1) - await usm.fund(deployer, deployer, 1) - await fum.transfer('0x0000000000000000000000000000000000000001', 1) -} \ No newline at end of file + await deployer.deploy(USM, oracle.address/*, wethAddresses[network]*/); +} diff --git a/test/03_USM.test.js b/test/03_USM.test.js index b84f2d8..a1dd70b 100644 --- a/test/03_USM.test.js +++ b/test/03_USM.test.js @@ -90,6 +90,10 @@ contract('USM', (accounts) => { }) describe("minting and burning", () => { + it("doesn't allow minting USM before minting FUM", async () => { + await expectRevert(usm.mint(user2, user1, totalEthToMint, { from: user2 }), "division by zero") + }) + it("allows minting FUM", async () => { const fumBuyPrice = (await usm.fumPrice(sides.BUY)) const fumSellPrice = (await usm.fumPrice(sides.SELL)) diff --git a/test/fuzzing/11_USM_Fuzzing.test.js b/test/fuzzing/11_USM_Fuzzing.test.js index 2121324..1e966ad 100644 --- a/test/fuzzing/11_USM_Fuzzing.test.js +++ b/test/fuzzing/11_USM_Fuzzing.test.js @@ -60,6 +60,11 @@ contract('USM', (accounts) => { usm = await USM.new(oracle.address, weth.address, { from: deployer }) fum = await FUM.at(await usm.fum()) + const initialFundEth = WAD + await weth.deposit({ from: user1, value: initialFundEth }) + await weth.approve(usm.address, initialFundEth, { from: user1 }) + await usm.fund(user1, user1, initialFundEth, { from: user1 }) + let snapshot = await timeMachine.takeSnapshot() snapshotId = snapshot['result'] }) @@ -77,7 +82,7 @@ contract('USM', (accounts) => { 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}`) From 2459924a7afdfb5cd5a95fd639b2340eec9f96a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Cuesta=20Ca=C3=B1ada?= Date: Thu, 8 Oct 2020 09:24:22 +0100 Subject: [PATCH 13/13] fix: the fuzzer helper now matches the fuzzing scenarios --- test/fuzzing/11_USM_Fuzzing.test.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/fuzzing/11_USM_Fuzzing.test.js b/test/fuzzing/11_USM_Fuzzing.test.js index 1e966ad..fda42f5 100644 --- a/test/fuzzing/11_USM_Fuzzing.test.js +++ b/test/fuzzing/11_USM_Fuzzing.test.js @@ -60,11 +60,6 @@ contract('USM', (accounts) => { usm = await USM.new(oracle.address, weth.address, { from: deployer }) fum = await FUM.at(await usm.fum()) - const initialFundEth = WAD - await weth.deposit({ from: user1, value: initialFundEth }) - await weth.approve(usm.address, initialFundEth, { from: user1 }) - await usm.fund(user1, user1, initialFundEth, { from: user1 }) - let snapshot = await timeMachine.takeSnapshot() snapshotId = snapshot['result'] })