From 20daa4ce4c413f57caee0f7d1e28609c886910fa Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 9 Sep 2020 18:15:25 -0400 Subject: [PATCH 01/14] feat(emp): add trimExcess function to send excess tokens to a predetermined address Signed-off-by: Matt Rice --- .../ExpiringMultiPartyCreator.sol | 2 ++ .../expiring-multiparty/Liquidatable.sol | 4 +++- .../PricelessPositionManager.sol | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol b/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol index a50c3f9969..5296f86da1 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol @@ -38,6 +38,7 @@ contract ExpiringMultiPartyCreator is ContractCreator, Testable, Lockable { FixedPoint.Unsigned minSponsorTokens; uint256 withdrawalLiveness; uint256 liquidationLiveness; + address beneficiary; } // - Address of TokenFactory to pass into newly constructed ExpiringMultiParty contracts address public tokenFactoryAddress; @@ -108,5 +109,6 @@ contract ExpiringMultiPartyCreator is ContractCreator, Testable, Lockable { constructorParams.minSponsorTokens = params.minSponsorTokens; constructorParams.withdrawalLiveness = params.withdrawalLiveness; constructorParams.liquidationLiveness = params.liquidationLiveness; + constructorParams.beneficiary = params.beneficiary; } } diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol b/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol index b573b002df..b9f953dd9e 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol @@ -61,6 +61,7 @@ contract Liquidatable is PricelessPositionManager { address finderAddress; address tokenFactoryAddress; address timerAddress; + address beneficiary; bytes32 priceFeedIdentifier; string syntheticName; string syntheticSymbol; @@ -160,7 +161,8 @@ contract Liquidatable is PricelessPositionManager { params.syntheticSymbol, params.tokenFactoryAddress, params.minSponsorTokens, - params.timerAddress + params.timerAddress, + params.beneficiary ) nonReentrant() { diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol index f408730704..9aa4ffe181 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol @@ -78,6 +78,9 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { // The expiry price pulled from the DVM. FixedPoint.Unsigned public expiryPrice; + // The beneficiary of any excess tokens added to the contract. + address public beneficiary; + /**************************************** * EVENTS * ****************************************/ @@ -157,7 +160,8 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { string memory _syntheticSymbol, address _tokenFactoryAddress, FixedPoint.Unsigned memory _minSponsorTokens, - address _timerAddress + address _timerAddress, + address _beneficiary ) public FeePayer(_collateralAddress, _finderAddress, _timerAddress) nonReentrant() { require(_expirationTimestamp > getCurrentTime(), "Invalid expiration in future"); require(_getIdentifierWhitelist().isIdentifierSupported(_priceIdentifier), "Unsupported price identifier"); @@ -168,6 +172,7 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { tokenCurrency = tf.createToken(_syntheticName, _syntheticSymbol, 18); minSponsorTokens = _minSponsorTokens; priceIdentifier = _priceIdentifier; + beneficiary = _beneficiary; } /**************************************** @@ -604,6 +609,18 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { return; } + function trimExcess(IERC20 token) external fees() nonReentrant() { + FixedPoint.Unsigned memory balance = FixedPoint.Unsigned(collateralCurrency.balanceOf(address(this))); + if (address(token) == address(collateralCurrency)) { + // If it is the collateral currency, send only the amount that the contract is not tracking. + // Note: this could be due to rounding error or balance-chainging tokens, like aTokens. + token.safeTransfer(beneficiary, balance.sub(pfc()).rawValue); + } else { + // If it's not the collateral currency, send the entire balance. + token.safeTransfer(beneficiary, balance.rawValue); + } + } + /** * @notice Accessor method for a sponsor's collateral. * @dev This is necessary because the struct returned by the positions() method shows From 2efc6171489fbd05a4978352084ade8a7e5048be Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Thu, 10 Sep 2020 09:54:43 -0400 Subject: [PATCH 02/14] comments Signed-off-by: Matt Rice --- .../ExpiringMultiPartyCreator.sol | 5 +++-- .../expiring-multiparty/Liquidatable.sol | 4 ++-- .../expiring-multiparty/PricelessPositionManager.sol | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol b/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol index 5296f86da1..de13a368f1 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/ExpiringMultiPartyCreator.sol @@ -38,7 +38,7 @@ contract ExpiringMultiPartyCreator is ContractCreator, Testable, Lockable { FixedPoint.Unsigned minSponsorTokens; uint256 withdrawalLiveness; uint256 liquidationLiveness; - address beneficiary; + address excessTokenBeneficiary; } // - Address of TokenFactory to pass into newly constructed ExpiringMultiParty contracts address public tokenFactoryAddress; @@ -94,6 +94,7 @@ contract ExpiringMultiPartyCreator is ContractCreator, Testable, Lockable { require(bytes(params.syntheticSymbol).length != 0, "Missing synthetic symbol"); require(params.withdrawalLiveness != 0, "Withdrawal liveness cannot be 0"); require(params.liquidationLiveness != 0, "Liquidation liveness cannot be 0"); + require(params.excessTokenBeneficiary != address(0), "Token Beneficiary cannot be 0x0"); _requireWhitelistedCollateral(params.collateralAddress); // Input from function call. @@ -109,6 +110,6 @@ contract ExpiringMultiPartyCreator is ContractCreator, Testable, Lockable { constructorParams.minSponsorTokens = params.minSponsorTokens; constructorParams.withdrawalLiveness = params.withdrawalLiveness; constructorParams.liquidationLiveness = params.liquidationLiveness; - constructorParams.beneficiary = params.beneficiary; + constructorParams.excessTokenBeneficiary = params.excessTokenBeneficiary; } } diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol b/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol index b9f953dd9e..9b9b193f8b 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/Liquidatable.sol @@ -61,7 +61,7 @@ contract Liquidatable is PricelessPositionManager { address finderAddress; address tokenFactoryAddress; address timerAddress; - address beneficiary; + address excessTokenBeneficiary; bytes32 priceFeedIdentifier; string syntheticName; string syntheticSymbol; @@ -162,7 +162,7 @@ contract Liquidatable is PricelessPositionManager { params.tokenFactoryAddress, params.minSponsorTokens, params.timerAddress, - params.beneficiary + params.excessTokenBeneficiary ) nonReentrant() { diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol index 9aa4ffe181..9d91708452 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol @@ -78,8 +78,8 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { // The expiry price pulled from the DVM. FixedPoint.Unsigned public expiryPrice; - // The beneficiary of any excess tokens added to the contract. - address public beneficiary; + // The excessTokenBeneficiary of any excess tokens added to the contract. + address public excessTokenBeneficiary; /**************************************** * EVENTS * @@ -161,7 +161,7 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { address _tokenFactoryAddress, FixedPoint.Unsigned memory _minSponsorTokens, address _timerAddress, - address _beneficiary + address _excessTokenBeneficiary ) public FeePayer(_collateralAddress, _finderAddress, _timerAddress) nonReentrant() { require(_expirationTimestamp > getCurrentTime(), "Invalid expiration in future"); require(_getIdentifierWhitelist().isIdentifierSupported(_priceIdentifier), "Unsupported price identifier"); @@ -172,7 +172,7 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { tokenCurrency = tf.createToken(_syntheticName, _syntheticSymbol, 18); minSponsorTokens = _minSponsorTokens; priceIdentifier = _priceIdentifier; - beneficiary = _beneficiary; + excessTokenBeneficiary = _excessTokenBeneficiary; } /**************************************** @@ -614,10 +614,10 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { if (address(token) == address(collateralCurrency)) { // If it is the collateral currency, send only the amount that the contract is not tracking. // Note: this could be due to rounding error or balance-chainging tokens, like aTokens. - token.safeTransfer(beneficiary, balance.sub(pfc()).rawValue); + token.safeTransfer(excessTokenBeneficiary, balance.sub(pfc()).rawValue); } else { // If it's not the collateral currency, send the entire balance. - token.safeTransfer(beneficiary, balance.rawValue); + token.safeTransfer(excessTokenBeneficiary, balance.rawValue); } } From 3ce5a7dff03059fb1a4d0ea6129c9fbfa30107e5 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Thu, 10 Sep 2020 16:28:45 -0400 Subject: [PATCH 03/14] add tests Signed-off-by: Matt Rice --- .../PricelessPositionManager.sol | 10 ++- .../core/scripts/DeploySyntheticTokens.js | 5 +- packages/core/scripts/local/DeployEMP.js | 7 +- .../financial-templates/ExpiringMultiParty.js | 3 +- .../ExpiringMultiPartyCreator.js | 23 ++++- .../test/financial-templates/Liquidatable.js | 41 ++++++++- .../PricelessPositionManager.js | 87 +++++++++++++++++-- packages/disputer/test/index.js | 3 +- packages/monitors/test/index.js | 3 +- 9 files changed, 163 insertions(+), 19 deletions(-) diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol index 9d91708452..27f07413b0 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol @@ -609,16 +609,18 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { return; } - function trimExcess(IERC20 token) external fees() nonReentrant() { - FixedPoint.Unsigned memory balance = FixedPoint.Unsigned(collateralCurrency.balanceOf(address(this))); + function trimExcess(IERC20 token) external fees() nonReentrant() returns (FixedPoint.Unsigned memory amount) { + FixedPoint.Unsigned memory balance = FixedPoint.Unsigned(token.balanceOf(address(this))); + if (address(token) == address(collateralCurrency)) { // If it is the collateral currency, send only the amount that the contract is not tracking. // Note: this could be due to rounding error or balance-chainging tokens, like aTokens. - token.safeTransfer(excessTokenBeneficiary, balance.sub(pfc()).rawValue); + amount = balance.sub(_pfc()); } else { // If it's not the collateral currency, send the entire balance. - token.safeTransfer(excessTokenBeneficiary, balance.rawValue); + amount = balance; } + token.safeTransfer(excessTokenBeneficiary, amount.rawValue); } /** diff --git a/packages/core/scripts/DeploySyntheticTokens.js b/packages/core/scripts/DeploySyntheticTokens.js index 25260643b2..7cffe60e1e 100644 --- a/packages/core/scripts/DeploySyntheticTokens.js +++ b/packages/core/scripts/DeploySyntheticTokens.js @@ -10,6 +10,7 @@ const AddressWhitelist = artifacts.require("AddressWhitelist"); const ExpiringMultiPartyCreator = artifacts.require("ExpiringMultiPartyCreator"); const IdentifierWhitelist = artifacts.require("IdentifierWhitelist"); const Registry = artifacts.require("Registry"); +const Store = artifacts.require("Store"); const collateral = { "Kovan DAI": "0x08ae34860fbfe73e223596e65663683973c72dd3" @@ -65,6 +66,7 @@ const actualDeploy = async inputCsv => { const deployer = (await web3.eth.getAccounts())[0]; const expiringMultiPartyCreator = await ExpiringMultiPartyCreator.deployed(); const identifierWhitelist = await IdentifierWhitelist.deployed(); + const store = await Store.deployed(); // Add EMP as a registered financial contract template factory. const registry = await Registry.deployed(); @@ -99,7 +101,8 @@ const actualDeploy = async inputCsv => { disputeBondPct: percentToFixedPoint(params.disputeBond), sponsorDisputeRewardPct: percentToFixedPoint(params.sponsorDisputeReward), disputerDisputeRewardPct: percentToFixedPoint(params.disputeReward), - minSponsorTokens: percentToFixedPoint(params.minSponsorTokens) + minSponsorTokens: percentToFixedPoint(params.minSponsorTokens), + excessTokenBeneficiary: store.address }; const address = await expiringMultiPartyCreator.createExpiringMultiParty.call(constructorParams); await expiringMultiPartyCreator.createExpiringMultiParty(constructorParams); diff --git a/packages/core/scripts/local/DeployEMP.js b/packages/core/scripts/local/DeployEMP.js index c5134fad31..8ad7273ad3 100644 --- a/packages/core/scripts/local/DeployEMP.js +++ b/packages/core/scripts/local/DeployEMP.js @@ -35,6 +35,7 @@ const WETH9 = artifacts.require("WETH9"); const Timer = artifacts.require("Timer"); const TokenFactory = artifacts.require("TokenFactory"); const AddressWhitelist = artifacts.require("AddressWhitelist"); +const Store = artifacts.require("Store"); const argv = require("minimist")(process.argv.slice(), { boolean: ["test"], string: ["identifier"] }); // Contracts we need to interact with. @@ -44,6 +45,7 @@ let mockOracle; let identifierWhitelist; let collateralTokenWhitelist; let expiringMultiPartyCreator; +let store; const empCollateralTokenMap = { COMPUSD: TestnetERC20, @@ -88,6 +90,8 @@ const deployEMP = async callback => { console.log("Whitelisted collateral currency"); } + store = await Store.deployed(); + // Create a new EMP const constructorParams = { expirationTimestamp: "1601503200", // 09/30/2020 @ 10:00pm (UTC) @@ -101,7 +105,8 @@ const deployEMP = async callback => { disputerDisputeRewardPct: { rawValue: toWei("0.2") }, minSponsorTokens: { rawValue: toWei("100") }, liquidationLiveness: 7200, - withdrawalLiveness: 7200 + withdrawalLiveness: 7200, + excessTokenBeneficiary: store.address }; let _emp = await expiringMultiPartyCreator.createExpiringMultiParty.call(constructorParams, { from: deployer }); diff --git a/packages/core/test/financial-templates/ExpiringMultiParty.js b/packages/core/test/financial-templates/ExpiringMultiParty.js index 2ea56e8f1b..ba8ab7119e 100644 --- a/packages/core/test/financial-templates/ExpiringMultiParty.js +++ b/packages/core/test/financial-templates/ExpiringMultiParty.js @@ -37,7 +37,8 @@ contract("ExpiringMultiParty", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: timer.address + timerAddress: timer.address, + excessTokenBeneficiary: accounts[0] }; identifierWhitelist = await IdentifierWhitelist.deployed(); diff --git a/packages/core/test/financial-templates/ExpiringMultiPartyCreator.js b/packages/core/test/financial-templates/ExpiringMultiPartyCreator.js index 82d512a98e..0590273701 100644 --- a/packages/core/test/financial-templates/ExpiringMultiPartyCreator.js +++ b/packages/core/test/financial-templates/ExpiringMultiPartyCreator.js @@ -1,5 +1,5 @@ const { toWei, hexToUtf8, toBN } = web3.utils; -const { didContractThrow } = require("@umaprotocol/common"); +const { didContractThrow, ZERO_ADDRESS } = require("@umaprotocol/common"); const truffleAssert = require("truffle-assertions"); // Tested Contract @@ -12,8 +12,7 @@ const Registry = artifacts.require("Registry"); const ExpiringMultiParty = artifacts.require("ExpiringMultiParty"); const IdentifierWhitelist = artifacts.require("IdentifierWhitelist"); const AddressWhitelist = artifacts.require("AddressWhitelist"); -const Timer = artifacts.require("Timer"); -const Finder = artifacts.require("Finder"); +const Store = artifacts.require("Store"); contract("ExpiringMultiPartyCreator", function(accounts) { let contractCreator = accounts[0]; @@ -23,6 +22,7 @@ contract("ExpiringMultiPartyCreator", function(accounts) { let expiringMultiPartyCreator; let registry; let collateralTokenWhitelist; + let store; // Re-used variables let constructorParams; @@ -36,6 +36,8 @@ contract("ExpiringMultiPartyCreator", function(accounts) { collateralTokenWhitelist = await AddressWhitelist.deployed(); await collateralTokenWhitelist.addToWhitelist(collateralToken.address, { from: contractCreator }); + store = await Store.deployed(); + constructorParams = { expirationTimestamp: "1625097600", collateralAddress: collateralToken.address, @@ -48,7 +50,8 @@ contract("ExpiringMultiPartyCreator", function(accounts) { disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, liquidationLiveness: 7200, - withdrawalLiveness: 7200 + withdrawalLiveness: 7200, + excessTokenBeneficiary: store.address }; identifierWhitelist = await IdentifierWhitelist.deployed(); @@ -130,6 +133,18 @@ contract("ExpiringMultiPartyCreator", function(accounts) { ); }); + it("Beneficiary cannot be 0x0", async function() { + // Change only the beneficiary address. + constructorParams.excessTokenBeneficiary = ZERO_ADDRESS; + assert( + await didContractThrow( + expiringMultiPartyCreator.createExpiringMultiParty(constructorParams, { + from: contractCreator + }) + ) + ); + }); + it("Can create new instances of ExpiringMultiParty", async function() { // Use `.call` to get the returned value from the function. let functionReturnedAddress = await expiringMultiPartyCreator.createExpiringMultiParty.call(constructorParams, { diff --git a/packages/core/test/financial-templates/Liquidatable.js b/packages/core/test/financial-templates/Liquidatable.js index 249fe84b26..89c3fbf4fb 100644 --- a/packages/core/test/financial-templates/Liquidatable.js +++ b/packages/core/test/financial-templates/Liquidatable.js @@ -27,6 +27,7 @@ contract("Liquidatable", function(accounts) { const liquidator = accounts[2]; const disputer = accounts[3]; const rando = accounts[4]; + const beneficiary = accounts[5]; const zeroAddress = "0x0000000000000000000000000000000000000000"; // Amount of tokens to mint for test @@ -133,7 +134,8 @@ contract("Liquidatable", function(accounts) { sponsorDisputeRewardPct: { rawValue: sponsorDisputeRewardPct.toString() }, disputerDisputeRewardPct: { rawValue: disputerDisputeRewardPct.toString() }, minSponsorTokens: { rawValue: minSponsorTokens.toString() }, - timerAddress: timer.address + timerAddress: timer.address, + excessTokenBeneficiary: beneficiary }; // Deploy liquidation contract and set global params @@ -173,6 +175,15 @@ contract("Liquidatable", function(accounts) { financialContractsAdmin = await FinancialContractsAdmin.deployed(); }); + const expectNoExcessCollateralToTrim = async () => { + let collateralTrimAmount = await liquidationContract.trimExcess.call(collateralToken.address); + await liquidationContract.trimExcess(collateralToken.address); + let beneficiaryCollateralBalance = await collateralToken.balanceOf(beneficiary); + + assert.equal(collateralTrimAmount.toString(), "0"); + assert.equal(beneficiaryCollateralBalance.toString(), "0"); + }; + describe("Attempting to liquidate a position that does not exist", () => { it("should revert", async () => { assert( @@ -343,6 +354,9 @@ contract("Liquidatable", function(accounts) { { from: liquidator } ); + // Check that excess collateral to be trimmed is still 0. + await expectNoExcessCollateralToTrim(); + // Collateral balance change should equal the final fee. assert.equal( intitialBalance.sub(await collateralToken.balanceOf(liquidator)).toString(), @@ -471,6 +485,9 @@ contract("Liquidatable", function(accounts) { assert.equal(expectedLockedCollateral.toString(), liquidation.lockedCollateral.toString()); assert.equal(expectedLiquidatedTokens.toString(), tokensLiquidated.toString()); + // Check that excess collateral to be trimmed is still 0. + await expectNoExcessCollateralToTrim(); + // A independent and identical liquidation can be created. await liquidationContract.createLiquidation( sponsor, @@ -726,6 +743,12 @@ contract("Liquidatable", function(accounts) { assert.equal(liquidation.disputer, disputer); assert.equal(liquidation.liquidationTime.toString(), liquidationTime.toString()); }); + it("Dispute generates no excess collateral", async () => { + await liquidationContract.dispute(liquidationParams.liquidationId, sponsor, { from: disputer }); + + // Check that excess collateral to be trimmed is still 0. + await expectNoExcessCollateralToTrim(); + }); it("Dispute emits an event", async () => { const disputeResult = await liquidationContract.dispute(liquidationParams.liquidationId, sponsor, { from: disputer @@ -874,6 +897,7 @@ contract("Liquidatable", function(accounts) { const liquidationTime = await liquidationContract.getCurrentTime(); const disputePrice = toWei("1"); await mockOracle.pushPrice(priceFeedIdentifier, liquidationTime, disputePrice); + await liquidationContract.withdrawLiquidation(liquidationParams.liquidationId, sponsor, { from: liquidator }); @@ -881,6 +905,9 @@ contract("Liquidatable", function(accounts) { const liquidation = await liquidationContract.liquidations(sponsor, liquidationParams.liquidationId); assert.equal(liquidation.state.toString(), LiquidationStatesEnum.DISPUTE_SUCCEEDED); + // Check that excess collateral to be trimmed is still 0 after the withdrawal. + await expectNoExcessCollateralToTrim(); + // We test that the event is emitted correctly for a successful dispute in a subsequent test. }); it("Dispute Failed", async () => { @@ -916,6 +943,9 @@ contract("Liquidatable", function(accounts) { ev.settlementPrice.toString() == disputePrice.toString() ); }); + + // Check that excess collateral to be trimmed is still 0 after the withdrawal. + await expectNoExcessCollateralToTrim(); }); it("Event correctly emitted", async () => { // Create a successful dispute and check the event is correct. @@ -1240,6 +1270,9 @@ contract("Liquidatable", function(accounts) { startBalance.add(toBN(sponsorAmount)).toString() ); + // Check that excess collateral to be trimmed is 0 after the sponsor withdraws. + await expectNoExcessCollateralToTrim(); + // Liquidator balance check. startBalance = await collateralToken.balanceOf(liquidator); await liquidationContract.withdrawLiquidation(liquidationParams.liquidationId, sponsor, { from: liquidator }); @@ -1248,6 +1281,9 @@ contract("Liquidatable", function(accounts) { startBalance.add(toBN(liquidatorAmount)).toString() ); + // Check that excess collateral to be trimmed is 0 afer the liquidator withdraws. + await expectNoExcessCollateralToTrim(); + // Disputer balance check. startBalance = await collateralToken.balanceOf(disputer); await liquidationContract.withdrawLiquidation(liquidationParams.liquidationId, sponsor, { from: disputer }); @@ -1256,6 +1292,9 @@ contract("Liquidatable", function(accounts) { startBalance.add(toBN(disputerAmount)).toString() ); + // Check that excess collateral to be trimmed is 0 after the last withdrawal. + await expectNoExcessCollateralToTrim(); + // Clean up store fees. await store.setFixedOracleFeePerSecondPerPfc({ rawValue: "0" }); }); diff --git a/packages/core/test/financial-templates/PricelessPositionManager.js b/packages/core/test/financial-templates/PricelessPositionManager.js index f9b497fd1e..dee2bdcd7b 100644 --- a/packages/core/test/financial-templates/PricelessPositionManager.js +++ b/packages/core/test/financial-templates/PricelessPositionManager.js @@ -2,6 +2,7 @@ const { PositionStatesEnum, didContractThrow } = require("@umaprotocol/common"); const truffleAssert = require("truffle-assertions"); const { interfaceName } = require("@umaprotocol/common"); +const { assert } = require("chai"); // Contracts to test const PricelessPositionManager = artifacts.require("PricelessPositionManager"); @@ -25,6 +26,7 @@ contract("PricelessPositionManager", function(accounts) { const tokenHolder = accounts[2]; const other = accounts[3]; const collateralOwner = accounts[4]; + const beneficiary = accounts[5]; // Contracts let collateral; @@ -69,18 +71,27 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(await collateral.balanceOf(pricelessPositionManager.address), expectedTotalCollateral.toString()); }; + const expectNoExcessCollateralToTrim = async () => { + let collateralTrimAmount = await pricelessPositionManager.trimExcess.call(collateral.address); + await pricelessPositionManager.trimExcess(collateral.address); + let beneficiaryCollateralBalance = await collateral.balanceOf(beneficiary); + + assert.equal(collateralTrimAmount.toString(), "0"); + assert.equal(beneficiaryCollateralBalance.toString(), "0"); + }; + before(async function() { + store = await Store.deployed(); + tokenFactory = await TokenFactory.deployed(); + }); + + beforeEach(async function() { // Represents DAI or some other token that the sponsor and contracts don't control. collateral = await MarginToken.new("UMA", "UMA", 18, { from: collateralOwner }); await collateral.addMember(1, collateralOwner, { from: collateralOwner }); await collateral.mint(sponsor, toWei("1000000"), { from: collateralOwner }); await collateral.mint(other, toWei("1000000"), { from: collateralOwner }); - store = await Store.deployed(); - tokenFactory = await TokenFactory.deployed(); - }); - - beforeEach(async function() { // Force each test to start with a simulated time that's synced to the startTimestamp. timer = await Timer.deployed(); await timer.setCurrentTime(startTimestamp); @@ -114,6 +125,7 @@ contract("PricelessPositionManager", function(accounts) { tokenFactory.address, // _tokenFactoryAddress { rawValue: minSponsorTokens }, // _minSponsorTokens timer.address, // _timerAddress + beneficiary, // _excessTokenBeneficiary { from: contractDeployer } ); tokenCurrency = await SyntheticToken.at(await pricelessPositionManager.tokenCurrency()); @@ -134,6 +146,7 @@ contract("PricelessPositionManager", function(accounts) { tokenFactory.address, // _tokenFactoryAddress { rawValue: minSponsorTokens }, // _minSponsorTokens timer.address, // _timerAddress + beneficiary, // _excessTokenBeneficiary { from: contractDeployer } ) ) @@ -153,6 +166,7 @@ contract("PricelessPositionManager", function(accounts) { tokenFactory.address, // _tokenFactoryAddress { rawValue: minSponsorTokens }, // _minSponsorTokens timer.address, // _timerAddress + beneficiary, // _excessTokenBeneficiary { from: contractDeployer } ) ) @@ -185,6 +199,8 @@ contract("PricelessPositionManager", function(accounts) { syntheticName, // _syntheticName (unchanged) syntheticSymbol, // _syntheticSymbol (unchanged) { rawValue: minSponsorTokens }, // _minSponsorTokens (unchanged) + timer.address, // _timerAddress (unchanged) + beneficiary, // _excessTokenBeneficiary (unchanged) { from: contractDeployer } ) ) @@ -208,6 +224,7 @@ contract("PricelessPositionManager", function(accounts) { tokenFactory.address, // _tokenFactoryAddress { rawValue: minSponsorTokens }, // _minSponsorTokens timer.address, // _timerAddress + beneficiary, // _excessTokenBeneficiary { from: contractDeployer } ); @@ -237,6 +254,9 @@ contract("PricelessPositionManager", function(accounts) { { from: other } ); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Create the initial pricelessPositionManager. const createTokens = toWei("100"); const createCollateral = toWei("150"); @@ -267,6 +287,9 @@ contract("PricelessPositionManager", function(accounts) { await checkBalances(expectedSponsorTokens, expectedSponsorCollateral); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Deposit. const depositCollateral = toWei("50"); expectedSponsorCollateral = expectedSponsorCollateral.add(toBN(depositCollateral)); @@ -280,6 +303,9 @@ contract("PricelessPositionManager", function(accounts) { await pricelessPositionManager.deposit({ rawValue: depositCollateral }, { from: sponsor }); await checkBalances(expectedSponsorTokens, expectedSponsorCollateral); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Withdraw. const withdrawCollateral = toWei("20"); expectedSponsorCollateral = expectedSponsorCollateral.sub(toBN(withdrawCollateral)); @@ -293,6 +319,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(sponsorFinalBalance.sub(sponsorInitialBalance).toString(), withdrawCollateral); await checkBalances(expectedSponsorTokens, expectedSponsorCollateral); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Redeem 50% of the tokens for 50% of the collateral. const redeemTokens = toWei("50"); expectedSponsorTokens = expectedSponsorTokens.sub(toBN(redeemTokens)); @@ -319,6 +348,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(sponsorFinalBalance.sub(sponsorInitialBalance).toString(), expectedSponsorCollateral); await checkBalances(expectedSponsorTokens, expectedSponsorCollateral); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Create additional. const createAdditionalTokens = toWei("10"); const createAdditionalCollateral = toWei("110"); @@ -332,6 +364,9 @@ contract("PricelessPositionManager", function(accounts) { ); await checkBalances(expectedSponsorTokens, expectedSponsorCollateral); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Redeem full. const redeemRemainingTokens = toWei("60"); await tokenCurrency.approve(pricelessPositionManager.address, redeemRemainingTokens, { from: sponsor }); @@ -352,6 +387,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(sponsorFinalBalance.sub(sponsorInitialBalance).toString(), expectedSponsorCollateral); await checkBalances(toBN("0"), toBN("0")); + // Periodic check for no excess collateral. + await expectNoExcessCollateralToTrim(); + // Contract state should not have changed. assert.equal(await pricelessPositionManager.contractState(), PositionStatesEnum.OPEN); }); @@ -731,6 +769,9 @@ contract("PricelessPositionManager", function(accounts) { return ev.caller == other; }); + // No excess collateral post expiry. + await expectNoExcessCollateralToTrim(); + // Settling an expired position should revert if the contract has expired but the DVM has not yet returned a price. assert(await didContractThrow(pricelessPositionManager.settleExpired({ from: tokenHolder }))); @@ -755,6 +796,9 @@ contract("PricelessPositionManager", function(accounts) { const tokenHolderFinalCollateral = await collateral.balanceOf(tokenHolder); const tokenHolderFinalSynthetic = await tokenCurrency.balanceOf(tokenHolder); + // No excess collateral post settlement. + await expectNoExcessCollateralToTrim(); + // The token holder should gain the value of their synthetic tokens in underlying. // The value in underlying is the number of tokens they held in the beginning * settlement price as TRV // When redeeming 50 tokens at a price of 1.2 we expect to receive 60 collateral tokens (50 * 1.2) @@ -824,6 +868,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(sponsorsPosition.withdrawalRequestPassTimestamp.toString(), 0); assert.equal(sponsorsPosition.transferPositionRequestPassTimestamp.toString(), 0); assert.equal(sponsorsPosition.withdrawalRequestAmount.rawValue, 0); + + // No excess collateral after all have settled. + await expectNoExcessCollateralToTrim(); }); it("Non sponsor can't deposit, redeem, withdraw, or transfer", async function() { @@ -1576,6 +1623,35 @@ contract("PricelessPositionManager", function(accounts) { assert(await didContractThrow(pricelessPositionManager.redeem({ rawValue: "16" }, { from: sponsor }))); }); + it("Can withdraw excess collateral", async function() { + // Attempt to redeem a position smaller s.t. the resulting position is less than 5 wei tokens (the min sponsor + // position size) + await collateral.approve(pricelessPositionManager.address, toWei("100000"), { from: sponsor }); + await tokenCurrency.approve(pricelessPositionManager.address, toWei("100000"), { from: sponsor }); + + await pricelessPositionManager.create({ rawValue: "40" }, { rawValue: "20" }, { from: sponsor }); + + // Transfer extra collateral in. + await collateral.transfer(pricelessPositionManager.address, web3.utils.toWei("10"), { from: sponsor }); + let excessCollateral = await pricelessPositionManager.trimExcess.call(collateral.address); + await pricelessPositionManager.trimExcess(collateral.address); + let beneficiaryCollateralBalance = await collateral.balanceOf(beneficiary); + assert.equal(excessCollateral.toString(), web3.utils.toWei("10")); + assert.equal(beneficiaryCollateralBalance.toString(), web3.utils.toWei("10")); + + // Transfer extra tokens in. + await tokenCurrency.transfer(pricelessPositionManager.address, "10", { from: sponsor }); + let excessTokens = await pricelessPositionManager.trimExcess.call(tokenCurrency.address); + await pricelessPositionManager.trimExcess(tokenCurrency.address); + let beneficiaryTokenBalance = await tokenCurrency.balanceOf(beneficiary); + assert.equal(excessTokens.toString(), "10"); + assert.equal(beneficiaryTokenBalance.toString(), "10"); + + // Redeem still succeeds. + await tokenCurrency.transfer(sponsor, "10", { from: beneficiary }); + await pricelessPositionManager.redeem({ rawValue: "20" }, { from: sponsor }); + }); + it("Non-standard ERC20 delimitation", async function() { // To test non-standard ERC20 token delimitation a new ERC20 token is created which has 6 decimal points of precision. // A new priceless position manager is then created and and set to use this token as collateral. To generate values @@ -1596,6 +1672,7 @@ contract("PricelessPositionManager", function(accounts) { tokenFactory.address, // _tokenFactoryAddress { rawValue: minSponsorTokens }, // _minSponsorTokens timer.address, // _timerAddress + beneficiary, // _excessTokenBeneficiary { from: contractDeployer } ); tokenCurrency = await SyntheticToken.at(await customPricelessPositionManager.tokenCurrency()); diff --git a/packages/disputer/test/index.js b/packages/disputer/test/index.js index 2c584e6cad..dc67685ea2 100644 --- a/packages/disputer/test/index.js +++ b/packages/disputer/test/index.js @@ -208,7 +208,8 @@ contract("index.js", function(accounts) { "UNKNOWN", constructorParams.tokenFactoryAddress, constructorParams.minSponsorTokens, - constructorParams.timerAddress + constructorParams.timerAddress, + contractCreator ); // We will also create a new spy logger, listening for debug events to validate the re-tries. diff --git a/packages/monitors/test/index.js b/packages/monitors/test/index.js index 968a67dfec..4a4b33e743 100644 --- a/packages/monitors/test/index.js +++ b/packages/monitors/test/index.js @@ -136,7 +136,8 @@ contract("index.js", function(accounts) { "UNKNOWN", constructorParams.tokenFactoryAddress, constructorParams.minSponsorTokens, - constructorParams.timerAddress + constructorParams.timerAddress, + contractCreator ); // Create a spy logger to catch all log messages to validate re-try attempts. From d1b18f6490bcb510f768745026ac6a2ee965d844 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Thu, 10 Sep 2020 17:34:09 -0400 Subject: [PATCH 04/14] WIP Signed-off-by: Matt Rice --- packages/core/test/financial-templates/Liquidatable.js | 4 ++++ .../test/financial-templates/PricelessPositionManager.js | 5 +++++ .../test/clients/ExpiringMultiPartyClient.js | 7 ++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/core/test/financial-templates/Liquidatable.js b/packages/core/test/financial-templates/Liquidatable.js index 89c3fbf4fb..5f2cd6e9b6 100644 --- a/packages/core/test/financial-templates/Liquidatable.js +++ b/packages/core/test/financial-templates/Liquidatable.js @@ -184,6 +184,10 @@ contract("Liquidatable", function(accounts) { assert.equal(beneficiaryCollateralBalance.toString(), "0"); }; + afterEach(async () => { + await expectNoExcessCollateralToTrim(); + }); + describe("Attempting to liquidate a position that does not exist", () => { it("should revert", async () => { assert( diff --git a/packages/core/test/financial-templates/PricelessPositionManager.js b/packages/core/test/financial-templates/PricelessPositionManager.js index dee2bdcd7b..7f95519bb6 100644 --- a/packages/core/test/financial-templates/PricelessPositionManager.js +++ b/packages/core/test/financial-templates/PricelessPositionManager.js @@ -131,6 +131,10 @@ contract("PricelessPositionManager", function(accounts) { tokenCurrency = await SyntheticToken.at(await pricelessPositionManager.tokenCurrency()); }); + afterEach(async () => { + await expectNoExcessCollateralToTrim(); + }); + it("Valid constructor params", async function() { // Expiration timestamp must be greater than contract current time. assert( @@ -1638,6 +1642,7 @@ contract("PricelessPositionManager", function(accounts) { let beneficiaryCollateralBalance = await collateral.balanceOf(beneficiary); assert.equal(excessCollateral.toString(), web3.utils.toWei("10")); assert.equal(beneficiaryCollateralBalance.toString(), web3.utils.toWei("10")); + await collateral.transfer(sponsor, web3.utils.toWei("10"), { from: beneficiary }); // Transfer extra tokens in. await tokenCurrency.transfer(pricelessPositionManager.address, "10", { from: sponsor }); diff --git a/packages/financial-templates-lib/test/clients/ExpiringMultiPartyClient.js b/packages/financial-templates-lib/test/clients/ExpiringMultiPartyClient.js index 85f593ca55..a5d14f0584 100644 --- a/packages/financial-templates-lib/test/clients/ExpiringMultiPartyClient.js +++ b/packages/financial-templates-lib/test/clients/ExpiringMultiPartyClient.js @@ -14,6 +14,7 @@ const MockOracle = artifacts.require("MockOracle"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const configs = [ { tokenName: "UMA", collateralDecimals: 18 }, @@ -37,6 +38,7 @@ contract("ExpiringMultiPartyClient.js", function(accounts) { let syntheticToken; let mockOracle; let identifierWhitelist; + let store; let identifier; let convert; @@ -62,6 +64,8 @@ contract("ExpiringMultiPartyClient.js", function(accounts) { identifierWhitelist = await IdentifierWhitelist.deployed(); await identifierWhitelist.addSupportedIdentifier(web3.utils.utf8ToHex(identifier)); + store = await Store.deployed(); + // Create a mockOracle and finder. Register the mockOracle with the finder. finder = await Finder.deployed(); mockOracle = await MockOracle.new(finder.address, Timer.address); @@ -85,7 +89,8 @@ contract("ExpiringMultiPartyClient.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; // The ExpiringMultiPartyClient does not emit any info `level` events. Therefore no need to test Winston outputs. From 57ede29b270208a1941306370c9f0382b2ec69bb Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Thu, 10 Sep 2020 18:46:07 -0400 Subject: [PATCH 05/14] WIP Signed-off-by: Matt Rice --- .../core/test/financial-templates/PricelessPositionManager.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/test/financial-templates/PricelessPositionManager.js b/packages/core/test/financial-templates/PricelessPositionManager.js index 7f95519bb6..7c83174b6f 100644 --- a/packages/core/test/financial-templates/PricelessPositionManager.js +++ b/packages/core/test/financial-templates/PricelessPositionManager.js @@ -1203,6 +1203,10 @@ contract("PricelessPositionManager", function(accounts) { assert.equal((await collateral.balanceOf(pricelessPositionManager.address)).toString(), "29"); assert.equal((await pricelessPositionManager.totalPositionCollateral()).toString(), "28"); assert.equal((await pricelessPositionManager.rawTotalPositionCollateral()).toString(), "30"); + + // Drain excess collateral left because of precesion loss. + await pricelessPositionManager.trimExcess(collateral.address); + await collateral.transfer(sponsor, (await collateral.balanceOf(beneficiary)).toString(), { from: beneficiary }); }); it("settleExpired() returns the same amount of collateral that totalPositionCollateral is decreased by", async () => { // Expire the contract From e81b8bf515d5984b4073390a5b1bb80c8c315c02 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Thu, 10 Sep 2020 23:55:29 -0400 Subject: [PATCH 06/14] Update PricelessPositionManager.js --- .../core/test/financial-templates/PricelessPositionManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/test/financial-templates/PricelessPositionManager.js b/packages/core/test/financial-templates/PricelessPositionManager.js index 7c83174b6f..770e5bd541 100644 --- a/packages/core/test/financial-templates/PricelessPositionManager.js +++ b/packages/core/test/financial-templates/PricelessPositionManager.js @@ -2,7 +2,6 @@ const { PositionStatesEnum, didContractThrow } = require("@umaprotocol/common"); const truffleAssert = require("truffle-assertions"); const { interfaceName } = require("@umaprotocol/common"); -const { assert } = require("chai"); // Contracts to test const PricelessPositionManager = artifacts.require("PricelessPositionManager"); From 59b7f52225c6f747835d913ef44ecdcaebb10376 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 11 Sep 2020 00:11:17 -0400 Subject: [PATCH 07/14] WIP Signed-off-by: Matt Rice --- .../PricelessPositionManager.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/core/test/financial-templates/PricelessPositionManager.js b/packages/core/test/financial-templates/PricelessPositionManager.js index 770e5bd541..ed823b2e0e 100644 --- a/packages/core/test/financial-templates/PricelessPositionManager.js +++ b/packages/core/test/financial-templates/PricelessPositionManager.js @@ -79,6 +79,16 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(beneficiaryCollateralBalance.toString(), "0"); }; + const expectAndDrainExcessCollateral = async () => { + // Drains the collateral from the contract and transfers it all back to the sponsor account to leave the beneficiary empty. + await pricelessPositionManager.trimExcess(collateral.address); + let beneficiaryCollateralBalance = await collateral.balanceOf(beneficiary); + collateral.transfer(sponsor, beneficiaryCollateralBalance.toString(), { from: beneficiary }); + + // Assert that nonzero collateral was drained. + assert.notEqual(beneficiaryCollateralBalance.toString(), "0"); + }; + before(async function() { store = await Store.deployed(); tokenFactory = await TokenFactory.deployed(); @@ -1204,8 +1214,7 @@ contract("PricelessPositionManager", function(accounts) { assert.equal((await pricelessPositionManager.rawTotalPositionCollateral()).toString(), "30"); // Drain excess collateral left because of precesion loss. - await pricelessPositionManager.trimExcess(collateral.address); - await collateral.transfer(sponsor, (await collateral.balanceOf(beneficiary)).toString(), { from: beneficiary }); + await expectAndDrainExcessCollateral(); }); it("settleExpired() returns the same amount of collateral that totalPositionCollateral is decreased by", async () => { // Expire the contract @@ -1298,6 +1307,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal(sponsorsPosition.withdrawalRequestPassTimestamp.toString(), 0); assert.equal(sponsorsPosition.transferPositionRequestPassTimestamp.toString(), 0); assert.equal(sponsorsPosition.withdrawalRequestAmount.rawValue, 0); + + // Drain excess collateral left because of precesion loss. + await expectAndDrainExcessCollateral(); }); it("withdraw() returns the same amount of collateral that totalPositionCollateral is decreased by", async () => { // The sponsor requests to withdraw 12 collateral. @@ -1319,6 +1331,9 @@ contract("PricelessPositionManager", function(accounts) { assert.equal((await collateral.balanceOf(pricelessPositionManager.address)).toString(), "18"); assert.equal((await pricelessPositionManager.totalPositionCollateral()).toString(), "17"); assert.equal((await pricelessPositionManager.rawTotalPositionCollateral()).toString(), "18"); + + // Drain excess collateral left because of precesion loss. + await expectAndDrainExcessCollateral(); }); it("redeem() returns the same amount of collateral that totalPositionCollateral is decreased by", async () => { // The sponsor requests to redeem 9 tokens. (9/20 = 0.45) tokens should result in a proportional redemption of the totalPositionCollateral, @@ -1340,6 +1355,9 @@ contract("PricelessPositionManager", function(accounts) { // Expected number of synthetic tokens are burned. assert.equal((await tokenCurrency.balanceOf(sponsor)).toString(), "11"); + + // Drain excess collateral left because of precesion loss. + await expectAndDrainExcessCollateral(); }); }); From b80606a7364a280369f04d84584f676ef2b2dca3 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 11 Sep 2020 00:18:54 -0400 Subject: [PATCH 08/14] WIP Signed-off-by: Matt Rice --- .../test/financial-templates/Liquidatable.js | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/core/test/financial-templates/Liquidatable.js b/packages/core/test/financial-templates/Liquidatable.js index 5f2cd6e9b6..dfc072241b 100644 --- a/packages/core/test/financial-templates/Liquidatable.js +++ b/packages/core/test/financial-templates/Liquidatable.js @@ -184,6 +184,16 @@ contract("Liquidatable", function(accounts) { assert.equal(beneficiaryCollateralBalance.toString(), "0"); }; + const expectAndDrainExcessCollateral = async () => { + // Drains the collateral from the contract and transfers it all back to the sponsor account to leave the beneficiary empty. + await liquidationContract.trimExcess(collateralToken.address); + let beneficiaryCollateralBalance = await collateralToken.balanceOf(beneficiary); + collateralToken.transfer(sponsor, beneficiaryCollateralBalance.toString(), { from: beneficiary }); + + // Assert that nonzero collateral was drained. + assert.notEqual(beneficiaryCollateralBalance.toString(), "0"); + }; + afterEach(async () => { await expectNoExcessCollateralToTrim(); }); @@ -1934,21 +1944,20 @@ contract("Liquidatable", function(accounts) { }); }); describe("Precision loss is handled as expected", () => { - let _liquidationContract; beforeEach(async () => { // Deploy a new Liquidation contract with no minimum sponsor token size. liquidatableParameters.minSponsorTokens = { rawValue: "0" }; - _liquidationContract = await Liquidatable.new(liquidatableParameters, { from: contractDeployer }); - syntheticToken = await Token.at(await _liquidationContract.tokenCurrency()); + liquidationContract = await Liquidatable.new(liquidatableParameters, { from: contractDeployer }); + syntheticToken = await Token.at(await liquidationContract.tokenCurrency()); // Create a new position with: // - 30 collateral // - 20 synthetic tokens (10 held by token holder, 10 by sponsor) - await collateralToken.approve(_liquidationContract.address, "100000", { from: sponsor }); + await collateralToken.approve(liquidationContract.address, "100000", { from: sponsor }); const numTokens = "20"; const amountCollateral = "30"; - await _liquidationContract.create({ rawValue: amountCollateral }, { rawValue: numTokens }, { from: sponsor }); - await syntheticToken.approve(_liquidationContract.address, numTokens, { from: sponsor }); + await liquidationContract.create({ rawValue: amountCollateral }, { rawValue: numTokens }, { from: sponsor }); + await syntheticToken.approve(liquidationContract.address, numTokens, { from: sponsor }); // Setting the regular fee to 4 % per second will result in a miscalculated cumulativeFeeMultiplier after 1 second // because of the intermediate calculation in `payRegularFees()` for calculating the `feeAdjustment`: ( fees paid ) / (total collateral) @@ -1958,19 +1967,19 @@ contract("Liquidatable", function(accounts) { await store.setFixedOracleFeePerSecondPerPfc({ rawValue: regularFee }); // Advance the contract one second and make the contract pay its regular fees - let startTime = await _liquidationContract.getCurrentTime(); - await _liquidationContract.setCurrentTime(startTime.addn(1)); - await _liquidationContract.payRegularFees(); + let startTime = await liquidationContract.getCurrentTime(); + await liquidationContract.setCurrentTime(startTime.addn(1)); + await liquidationContract.payRegularFees(); // Set the store fees back to 0 to prevent fee multiplier from changing for remainder of the test. await store.setFixedOracleFeePerSecondPerPfc({ rawValue: "0" }); // Set allowance for contract to pull synthetic tokens from liquidator - await syntheticToken.increaseAllowance(_liquidationContract.address, numTokens, { from: liquidator }); + await syntheticToken.increaseAllowance(liquidationContract.address, numTokens, { from: liquidator }); await syntheticToken.transfer(liquidator, numTokens, { from: sponsor }); // Create a liquidation. - await _liquidationContract.createLiquidation( + await liquidationContract.createLiquidation( sponsor, { rawValue: "0" }, { rawValue: toWei("1.5") }, @@ -1986,10 +1995,10 @@ contract("Liquidatable", function(accounts) { // 1/30. However, 1/30 = 0.03333... repeating, which cannot be represented in FixedPoint. Normally div() would floor // this value to 0.033....33, but divCeil sets this to 0.033...34. A higher `feeAdjustment` causes a lower `adjustment` and ultimately // lower `totalPositionCollateral` and `positionAdjustment` values. - let collateralAmount = await _liquidationContract.getCollateral(sponsor); + let collateralAmount = await liquidationContract.getCollateral(sponsor); assert(toBN(collateralAmount.rawValue).lt(toBN("29"))); assert.equal( - (await _liquidationContract.cumulativeFeeMultiplier()).toString(), + (await liquidationContract.cumulativeFeeMultiplier()).toString(), toWei("0.966666666666666666").toString() ); @@ -1998,12 +2007,15 @@ contract("Liquidatable", function(accounts) { // because `(30 * 0.966666666666666666 = 28.999...98)`. `30` is the rawCollateral and if the fee multiplier were correct, // then `rawLiquidationCollateral` would be `(30 * 0.966666666666666666...) = 29`. // `rawTotalPositionCollateral` is decreased after `createLiquidation()` is called. - assert.equal((await collateralToken.balanceOf(_liquidationContract.address)).toString(), "29"); - assert.equal((await _liquidationContract.rawLiquidationCollateral()).toString(), "28"); - assert.equal((await _liquidationContract.rawTotalPositionCollateral()).toString(), "0"); + assert.equal((await collateralToken.balanceOf(liquidationContract.address)).toString(), "29"); + assert.equal((await liquidationContract.rawLiquidationCollateral()).toString(), "28"); + assert.equal((await liquidationContract.rawTotalPositionCollateral()).toString(), "0"); + + // Check that the excess collateral can be drained. + await expectAndDrainExcessCollateral(); }); it("Liquidation object is set up properly", async () => { - let liquidationData = await _liquidationContract.liquidations(sponsor, 0); + let liquidationData = await liquidationContract.liquidations(sponsor, 0); // The contract should own 29 collateral but show locked collateral in the liquidation as 28, using the same calculation // as `totalPositionCollateral` which is `rawTotalPositionCollateral` from the liquidated position multiplied by the fee multiplier. @@ -2018,6 +2030,9 @@ contract("Liquidatable", function(accounts) { // locked collateral. // - rawUnitCollateral = (1 / 0.966666666666666666) = 1.034482758620689655 assert.equal(fromWei(liquidationData.rawUnitCollateral.toString()), "1.034482758620689655"); + + // Check that the excess collateral can be drained. + await expectAndDrainExcessCollateral(); }); it("withdrawLiquidation() returns the same amount of collateral that liquidationCollateral is decreased by", async () => { // So, the available collateral for rewards should be (lockedCollateral * feeAttenuation), @@ -2026,8 +2041,8 @@ contract("Liquidatable", function(accounts) { // will decrease by less than its full lockedCollateral. The contract should transfer to the liquidator the same amount. // First, expire the liquidation - let startTime = await _liquidationContract.getCurrentTime(); - await _liquidationContract.setCurrentTime( + let startTime = await liquidationContract.getCurrentTime(); + await liquidationContract.setCurrentTime( toBN(startTime) .add(liquidationLiveness) .toString() @@ -2035,14 +2050,17 @@ contract("Liquidatable", function(accounts) { // The liquidator is owed (0.999999999999999999 * 28 = 27.9999...) which gets truncated to 27. // The contract should have 29 - 27 = 2 collateral remaining, and the liquidation should be deleted. - await _liquidationContract.withdrawLiquidation(0, sponsor, { from: liquidator }); + await liquidationContract.withdrawLiquidation(0, sponsor, { from: liquidator }); assert.equal((await collateralToken.balanceOf(liquidator)).toString(), "27"); - assert.equal((await collateralToken.balanceOf(_liquidationContract.address)).toString(), "2"); - let deletedLiquidationData = await _liquidationContract.liquidations(sponsor, 0); + assert.equal((await collateralToken.balanceOf(liquidationContract.address)).toString(), "2"); + let deletedLiquidationData = await liquidationContract.liquidations(sponsor, 0); assert.equal(deletedLiquidationData.state.toString(), LiquidationStatesEnum.UNINITIALIZED); // rawLiquidationCollateral should also have been decreased by 27, from 28 to 1 - assert.equal((await _liquidationContract.rawLiquidationCollateral()).toString(), "1"); + assert.equal((await liquidationContract.rawLiquidationCollateral()).toString(), "1"); + + // Check that the excess collateral can be drained. + await expectAndDrainExcessCollateral(); }); }); }); From d8ffc75b6ec2a6984d932ac23bf232b435962689 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 11 Sep 2020 09:00:59 -0400 Subject: [PATCH 09/14] WIP Signed-off-by: Matt Rice --- .../clients/ExpiringMultiPartyEventClient.js | 3 ++- .../test/price-feed/CreatePriceFeed.js | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/financial-templates-lib/test/clients/ExpiringMultiPartyEventClient.js b/packages/financial-templates-lib/test/clients/ExpiringMultiPartyEventClient.js index cd5e562715..ac69eaf50e 100644 --- a/packages/financial-templates-lib/test/clients/ExpiringMultiPartyEventClient.js +++ b/packages/financial-templates-lib/test/clients/ExpiringMultiPartyEventClient.js @@ -105,7 +105,8 @@ contract("ExpiringMultiPartyEventClient.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: timer.address + timerAddress: timer.address, + excessTokenBeneficiary: store.address }; emp = await ExpiringMultiParty.new(constructorParams); diff --git a/packages/financial-templates-lib/test/price-feed/CreatePriceFeed.js b/packages/financial-templates-lib/test/price-feed/CreatePriceFeed.js index 90f31c0858..d290dba901 100644 --- a/packages/financial-templates-lib/test/price-feed/CreatePriceFeed.js +++ b/packages/financial-templates-lib/test/price-feed/CreatePriceFeed.js @@ -9,6 +9,7 @@ const IdentifierWhitelist = artifacts.require("IdentifierWhitelist"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const { createPriceFeed, @@ -28,6 +29,7 @@ contract("CreatePriceFeed.js", function(accounts) { let mockTime = 1588376548; let networker; + let store; const apiKey = "test-api-key"; const exchange = "test-exchange"; @@ -44,6 +46,7 @@ contract("CreatePriceFeed.js", function(accounts) { logger = winston.createLogger({ silent: true }); + store = await Store.deployed(); }); it("No type", async function() { @@ -185,7 +188,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; const emp = await ExpiringMultiParty.new(constructorParams); @@ -227,7 +231,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; const emp = await ExpiringMultiParty.new(constructorParams); @@ -290,7 +295,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; const emp = await ExpiringMultiParty.new(constructorParams); @@ -323,7 +329,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; const emp = await ExpiringMultiParty.new(constructorParams); @@ -349,7 +356,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; const emp = await ExpiringMultiParty.new(constructorParams); @@ -471,7 +479,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; let emp = await ExpiringMultiParty.new(constructorParams); @@ -506,7 +515,8 @@ contract("CreatePriceFeed.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; identifierWhitelist = await IdentifierWhitelist.deployed(); From 1efe2c7d908fd7875083b91da8a7d862f994e91c Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 11 Sep 2020 10:22:01 -0400 Subject: [PATCH 10/14] WIP Signed-off-by: Matt Rice --- packages/disputer/test/Disputer.js | 6 +++++- packages/monitors/test/CRMonitor.js | 5 ++++- packages/monitors/test/ContractMonitor.js | 5 ++++- packages/monitors/test/index.js | 5 ++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/disputer/test/Disputer.js b/packages/disputer/test/Disputer.js index 6c32d9091a..2c7924e2a2 100644 --- a/packages/disputer/test/Disputer.js +++ b/packages/disputer/test/Disputer.js @@ -29,6 +29,7 @@ const MockOracle = artifacts.require("MockOracle"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const configs = [ { tokenName: "UMA", collateralDecimals: 18 }, { tokenName: "BTC", collateralDecimals: 8 } @@ -50,6 +51,7 @@ contract("Disputer.js", function(accounts) { let emp; let syntheticToken; let mockOracle; + let store; let spy; let spyLogger; @@ -94,6 +96,7 @@ contract("Disputer.js", function(accounts) { }); const mockOracleInterfaceName = web3.utils.utf8ToHex(interfaceName.Oracle); await finder.changeImplementationAddress(mockOracleInterfaceName, mockOracle.address); + store = await Store.deployed(); const constructorParams = { expirationTimestamp: "20345678900", @@ -110,7 +113,8 @@ contract("Disputer.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; identifierWhitelist = await IdentifierWhitelist.deployed(); diff --git a/packages/monitors/test/CRMonitor.js b/packages/monitors/test/CRMonitor.js index af250a5587..5df2a741cf 100644 --- a/packages/monitors/test/CRMonitor.js +++ b/packages/monitors/test/CRMonitor.js @@ -23,6 +23,7 @@ const MockOracle = artifacts.require("MockOracle"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const configs = [ { tokenName: "WETH", collateralDecimals: 18 }, @@ -94,6 +95,7 @@ contract("CRMonitor.js", function(accounts) { timer = await Timer.deployed(); await timer.setCurrentTime(currentTime.toString()); expirationTime = currentTime.toNumber() + 100; // 100 seconds in the future + const store = await Store.deployed(); constructorParams = { isTest: true, @@ -111,7 +113,8 @@ contract("CRMonitor.js", function(accounts) { disputeBondPct: { rawValue: toWei("0.1") }, sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, - minSponsorTokens: { rawValue: toWei("1") } + minSponsorTokens: { rawValue: toWei("1") }, + excessTokenBeneficiary: store.address }; // Create a sinon spy and give it to the SpyTransport as the winston logger. Use this to check all winston diff --git a/packages/monitors/test/ContractMonitor.js b/packages/monitors/test/ContractMonitor.js index 7eee75e219..330a2a46de 100644 --- a/packages/monitors/test/ContractMonitor.js +++ b/packages/monitors/test/ContractMonitor.js @@ -23,6 +23,7 @@ const MockOracle = artifacts.require("MockOracle"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const configs = [ { tokenName: "WETH", collateralDecimals: 18 }, @@ -92,6 +93,7 @@ contract("ContractMonitor.js", function(accounts) { const timer = await Timer.deployed(); await timer.setCurrentTime(currentTime.toString()); expirationTime = currentTime.toNumber() + 100; // 100 seconds in the future + const store = await Store.deployed(); constructorParams = { isTest: true, @@ -109,7 +111,8 @@ contract("ContractMonitor.js", function(accounts) { disputeBondPct: { rawValue: toWei("0.1") }, sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, - minSponsorTokens: { rawValue: toWei("1") } + minSponsorTokens: { rawValue: toWei("1") }, + excessTokenBeneficiary: store.address }; // Create a sinon spy and give it to the SpyTransport as the winston logger. Use this to check all winston diff --git a/packages/monitors/test/index.js b/packages/monitors/test/index.js index 4a4b33e743..3fac9147ce 100644 --- a/packages/monitors/test/index.js +++ b/packages/monitors/test/index.js @@ -12,6 +12,7 @@ const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); const UniswapMock = artifacts.require("UniswapMock"); +const Store = artifacts.require("Store"); // Custom winston transport module to monitor winston log outputs const winston = require("winston"); @@ -54,6 +55,7 @@ contract("index.js", function(accounts) { level: "info", transports: [new SpyTransport({ level: "info" }, { spy: spy })] }); + const store = await Store.deployed(); constructorParams = { expirationTimestamp: "12345678900", @@ -70,7 +72,8 @@ contract("index.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; // Deploy a new expiring multi party From 9497eb908213cc1026b945e27031d0f7ca9e9729 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 11 Sep 2020 16:39:59 -0400 Subject: [PATCH 11/14] WIP Signed-off-by: Matt Rice --- packages/disputer/test/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/disputer/test/index.js b/packages/disputer/test/index.js index dc67685ea2..343e37c829 100644 --- a/packages/disputer/test/index.js +++ b/packages/disputer/test/index.js @@ -12,6 +12,7 @@ const Token = artifacts.require("ExpandedERC20"); const TokenFactory = artifacts.require("TokenFactory"); const Timer = artifacts.require("Timer"); const UniswapMock = artifacts.require("UniswapMock"); +const Store = artifacts.require("Store"); // Custom winston transport module to monitor winston log outputs const winston = require("winston"); @@ -27,6 +28,7 @@ contract("index.js", function(accounts) { let defaultPriceFeedConfig; let constructorParams; + let store; let spy; let spyLogger; @@ -51,6 +53,8 @@ contract("index.js", function(accounts) { transports: [new SpyTransport({ level: "info" }, { spy: spy })] }); + store = await Store.deployed(); + constructorParams = { expirationTimestamp: "20345678900", withdrawalLiveness: "1000", @@ -66,7 +70,8 @@ contract("index.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; // Deploy a new expiring multi party @@ -109,7 +114,8 @@ contract("index.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; emp = await ExpiringMultiParty.new(constructorParams); From bc6e302a11a2ef8c43f36067bb43fe2ee56dac70 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 14 Sep 2020 14:44:04 -0400 Subject: [PATCH 12/14] WIP Signed-off-by: Matt Rice --- packages/liquidator/test/Liquidator.js | 5 ++++- packages/liquidator/test/index.js | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/liquidator/test/Liquidator.js b/packages/liquidator/test/Liquidator.js index 4c18a2c44a..d758a81ca5 100644 --- a/packages/liquidator/test/Liquidator.js +++ b/packages/liquidator/test/Liquidator.js @@ -35,6 +35,7 @@ const MockOracle = artifacts.require("MockOracle"); const TokenFactory = artifacts.require("TokenFactory"); const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); +const Store = artifacts.require("Store"); const configs = [ { tokenName: "WETH", collateralDecimals: 18 }, @@ -115,6 +116,7 @@ contract("Liquidator.js", function(accounts) { }); const mockOracleInterfaceName = utf8ToHex(interfaceName.Oracle); await finder.changeImplementationAddress(mockOracleInterfaceName, mockOracle.address); + const store = await Store.deployed(); const constructorParams = { expirationTimestamp: "20345678900", @@ -131,7 +133,8 @@ contract("Liquidator.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("5") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; // Deploy a new expiring multi party diff --git a/packages/liquidator/test/index.js b/packages/liquidator/test/index.js index f026aedfbe..1054cd22fa 100644 --- a/packages/liquidator/test/index.js +++ b/packages/liquidator/test/index.js @@ -12,6 +12,7 @@ const Token = artifacts.require("ExpandedERC20"); const Timer = artifacts.require("Timer"); const UniswapMock = artifacts.require("UniswapMock"); const OneSplitMock = artifacts.require("OneSplitMock"); +const Store = artifacts.require("Store"); // Custom winston transport module to monitor winston log outputs const winston = require("winston"); @@ -26,6 +27,7 @@ contract("index.js", function(accounts) { let syntheticToken; let emp; let uniswap; + let store; let defaultPriceFeedConfig; @@ -46,6 +48,7 @@ contract("index.js", function(accounts) { await identifierWhitelist.addSupportedIdentifier(utf8ToHex("ETH/BTC")); oneSplitMock = await OneSplitMock.new(); + store = await Store.deployed(); constructorParams = { expirationTimestamp: "20345678900", @@ -62,7 +65,8 @@ contract("index.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; }); @@ -116,7 +120,8 @@ contract("index.js", function(accounts) { sponsorDisputeRewardPct: { rawValue: toWei("0.1") }, disputerDisputeRewardPct: { rawValue: toWei("0.1") }, minSponsorTokens: { rawValue: toWei("1") }, - timerAddress: Timer.address + timerAddress: Timer.address, + excessTokenBeneficiary: store.address }; emp = await ExpiringMultiParty.new(constructorParams); From 332b56eba61bf0dd34adedf81beefa51c16f917a Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 14 Sep 2020 17:36:37 -0400 Subject: [PATCH 13/14] Update PricelessPositionManager.sol --- .../expiring-multiparty/PricelessPositionManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol index 27f07413b0..e160a748d8 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol @@ -614,7 +614,7 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { if (address(token) == address(collateralCurrency)) { // If it is the collateral currency, send only the amount that the contract is not tracking. - // Note: this could be due to rounding error or balance-chainging tokens, like aTokens. + // Note: this could be due to rounding error or balance-changing tokens, like aTokens. amount = balance.sub(_pfc()); } else { // If it's not the collateral currency, send the entire balance. From 6d192dec7b9d63442672073028779ecc3a045674 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 14 Sep 2020 17:44:41 -0400 Subject: [PATCH 14/14] WIP Signed-off-by: Matt Rice --- .../expiring-multiparty/PricelessPositionManager.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol index e160a748d8..3e10fceb3a 100644 --- a/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol +++ b/packages/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol @@ -148,6 +148,8 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { * @param _tokenFactoryAddress deployed UMA token factory to create the synthetic token. * @param _minSponsorTokens minimum amount of collateral that must exist at any time in a position. * @param _timerAddress Contract that stores the current time in a testing environment. + * @param _excessTokenBeneficiary Beneficiary to which all excess token balances that accrue in the contract can be + * sent. * Must be set to 0x0 for production environments that use live time. */ constructor( @@ -609,6 +611,11 @@ contract PricelessPositionManager is FeePayer, AdministrateeInterface { return; } + /** + * @notice Drains any excess balance of the provided ERC20 token to a pre-selected beneficiary. + * @dev This will drain down to the amount of tracked collateral and drain the full balance of any other token. + * @param token address of the ERC20 token whose excess balance should be drained. + */ function trimExcess(IERC20 token) external fees() nonReentrant() returns (FixedPoint.Unsigned memory amount) { FixedPoint.Unsigned memory balance = FixedPoint.Unsigned(token.balanceOf(address(this)));