diff --git a/README.md b/README.md index a4d5e1d..71041ac 100644 --- a/README.md +++ b/README.md @@ -408,8 +408,8 @@ Testing on rinkeby has been deprecated. | Contract | Info | Address | |---------------------|-------------------------------|-----------------------------------------------------------------------------------------------------------------------| | rDAI proxy | Use this address in your dapp | [0x261b45D85cCFeAbb11F022eBa346ee8D1cd488c0](https://etherscan.io/address/0x261b45d85ccfeabb11f022eba346ee8d1cd488c0) | -| rToken logic | Version 1.0.1-rc2 | [0x806a196872Beff0CDE5663cbc1d8962309444932](https://etherscan.io/address/0x806a196872beff0cde5663cbc1d8962309444932) | -| Allocation Strategy | Compound | [0xd0810DeE68dD9aEAfc2dBC2A6f53C3809A2E6578](https://etherscan.io/address/0xd0810dee68dd9aeafc2dbc2a6f53c3809a2e6578) | +| rToken logic | Version 1.0.1-rc2 | [0x56b481bA9f338144Fa40C84fb0F4C87B9f4d6dFE](https://etherscan.io/address/0x56b481bA9f338144Fa40C84fb0F4C87B9f4d6dFE) | +| Allocation Strategy | Compound | [0xbB16307aaed1e070B3C4465d4FDa5E518bDc2433](https://etherscan.io/address/0xbB16307aaed1e070B3C4465d4FDa5E518bDc2433) | | Underlying token | DAI | [0x6B175474E89094C44Da98b954EedeAC495271d0F](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F) | | Allocation token | cDAI | [0x5d3a536e4d6dbd6114cc1ead35777bab948e3643](https://etherscan.io/address/0x5d3a536e4d6dbd6114cc1ead35777bab948e3643) | diff --git a/contracts/CompoundAllocationStrategy.sol b/contracts/CompoundAllocationStrategy.sol index 7638fdd..907b2f5 100644 --- a/contracts/CompoundAllocationStrategy.sol +++ b/contracts/CompoundAllocationStrategy.sol @@ -48,7 +48,7 @@ contract CompoundAllocationStrategy is IAllocationStrategy, Ownable { function redeemUnderlying(uint256 redeemAmount) external onlyOwner returns (uint256) { uint256 cTotalBefore = cToken.totalSupply(); // TODO should we handle redeem failure? - require(cToken.redeemUnderlying(redeemAmount) == 0, "redeemUnderlying failed"); + require(cToken.redeemUnderlying(redeemAmount) == 0, "cToken.redeemUnderlying failed"); uint256 cTotalAfter = cToken.totalSupply(); uint256 cBurnedAmount; require(cTotalAfter <= cTotalBefore, "Compound redeemed negative amount!?"); @@ -57,4 +57,13 @@ contract CompoundAllocationStrategy is IAllocationStrategy, Ownable { return cBurnedAmount; } + /// @dev ISavingStrategy.redeemAll implementation + function redeemAll() external onlyOwner + returns (uint256 savingsAmount, uint256 underlyingAmount) { + savingsAmount = cToken.balanceOf(address(this)); + require(cToken.redeem(savingsAmount) == 0, "cToken.redeem failed"); + underlyingAmount = token.balanceOf(address(this)); + token.transfer(msg.sender, underlyingAmount); + } + } diff --git a/contracts/IAllocationStrategy.sol b/contracts/IAllocationStrategy.sol index 5b5272f..7314faa 100644 --- a/contracts/IAllocationStrategy.sol +++ b/contracts/IAllocationStrategy.sol @@ -4,7 +4,7 @@ pragma solidity ^0.5.8; * @notice Allocation strategy for assets. * - It invests the underlying assets into some yield generating contracts, * usually lending contracts, in return it gets new assets aka. saving assets. - * - Sainv assets can be redeemed back to the underlying assets plus interest any time. + * - Savings assets can be redeemed back to the underlying assets plus interest any time. */ interface IAllocationStrategy { @@ -48,4 +48,12 @@ interface IAllocationStrategy { */ function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + /** + * @notice Owner redeems all saving assets + * @dev Interst shall be accrued + * @return uint256 savingsAmount Amount of savings redeemed + * @return uint256 underlyingAmount Amount of underlying redeemed + */ + function redeemAll() external returns (uint256 savingsAmount, uint256 underlyingAmount); + } diff --git a/contracts/RToken.sol b/contracts/RToken.sol index c77be96..30e4fa9 100644 --- a/contracts/RToken.sol +++ b/contracts/RToken.sol @@ -426,10 +426,22 @@ contract RToken is IAllocationStrategy oldIas = ias; ias = allocationStrategy; // redeem everything from the old strategy - uint256 sOriginalBurned = oldIas.redeemUnderlying(totalSupply); + (uint256 sOriginalBurned, ) = oldIas.redeemAll(); + uint256 totalAmount = token.balanceOf(address(this)); // invest everything into the new strategy - require(token.approve(address(ias), totalSupply), "token approve failed"); - uint256 sOriginalCreated = ias.investUnderlying(totalSupply); + require(token.approve(address(ias), totalAmount), "token approve failed"); + uint256 sOriginalCreated = ias.investUnderlying(totalAmount); + + // give back the ownership of the old allocation strategy to the admin + // unless we are simply switching to the same allocaiton Strategy + // + // - But why would we switch to the same allocation strategy? + // - This is a special case where one could pick up the unsoliciated + // savings from the allocation srategy contract as extra "interest" + // for all rToken holders. + if (address(ias) != address(oldIas)) { + Ownable(address(oldIas)).transferOwnership(address(owner())); + } // calculate new saving asset conversion rate // diff --git a/package.json b/package.json index 2982530..bf5d857 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rtoken/contracts", - "version": "1.0.1-rc3", + "version": "1.0.1-rc4", "description": "RToken Ethereum contracts", "license": "MIT", "dependencies": { diff --git a/scripts/deploy-rdai-logic.js b/scripts/deploy-rdai-logic.js index 9547e25..06fc250 100644 --- a/scripts/deploy-rdai-logic.js +++ b/scripts/deploy-rdai-logic.js @@ -7,11 +7,7 @@ module.exports = async function (callback) { const { web3tx } = require("@decentral.ee/web3-test-helpers"); const RToken = artifacts.require("rDAI"); - const rDaiLogic = await web3tx(RToken.new, "RToken.new")( - { - gas: 5000000, - } - ); + const rDaiLogic = await web3tx(RToken.new, "RToken.new")(); console.log("rDaiLogic deployed at: ", rDaiLogic.address); callback(); diff --git a/scripts/deploy-rdai.js b/scripts/deploy-rdai.js index 64e19e2..133f262 100644 --- a/scripts/deploy-rdai.js +++ b/scripts/deploy-rdai.js @@ -26,7 +26,7 @@ module.exports = async function (callback) { const RDAI = artifacts.require("rDAI"); const Proxy = artifacts.require("Proxy"); - const addresses = await require("./addresses")[network]; + const addresses = require("./addresses")[network]; let compoundASAddress = await promisify(rl.question)("Specify a deployed CompoundAllocationStrategy (deploy a new one if blank): "); let compoundAS; @@ -34,9 +34,7 @@ module.exports = async function (callback) { compoundAS = await web3tx( CompoundAllocationStrategy.new, `CompoundAllocationStrategy.new cDAI ${addresses.cDAI}`)( - addresses.cDAI, { - gas: 1000000, - } + addresses.cDAI ); console.log("compoundAllocationStrategy deployed at: ", compoundAS.address); } else { @@ -46,11 +44,7 @@ module.exports = async function (callback) { let rDAIAddress = await promisify(rl.question)("Specify a deployed rDAI (deploy a new one if blank): "); let rDAI; if (!rDAIAddress) { - rDAI = await web3tx(RDAI.new, "rDAI.new")( - { - gas: 5000000, - } - ); + rDAI = await web3tx(RDAI.new, "rDAI.new")(); console.log("rDAI deployed at: ", rDAI.address); } else { rDAI = await RDAI.at(rDAIAddress); @@ -59,9 +53,7 @@ module.exports = async function (callback) { const rDaiConstructCode = rDAI.contract.methods.initialize(compoundAS.address).encodeABI(); console.log(`rDaiConstructCode rDAI.initialize(${rDaiConstructCode})`); const proxy = await web3tx(Proxy.new, "Proxy.new")( - rDaiConstructCode, rDAI.address, { - gas: 1000000, - } + rDaiConstructCode, rDAI.address ); console.log("proxy deployed at: ", proxy.address); diff --git a/scripts/deploy-rsai-logic.js b/scripts/deploy-rsai-logic.js index 4010e8f..2932fbd 100644 --- a/scripts/deploy-rsai-logic.js +++ b/scripts/deploy-rsai-logic.js @@ -7,11 +7,7 @@ module.exports = async function (callback) { const { web3tx } = require("@decentral.ee/web3-test-helpers"); const rSAI = artifacts.require("rSAI"); - const rSAILogic = await web3tx(rSAI.new, "rSAI.new")( - { - gas: 5000000, - } - ); + const rSAILogic = await web3tx(rSAI.new, "rSAI.new")(); console.log("rSAILogic deployed at: ", rSAILogic.address); callback(); diff --git a/test/RToken.test.js b/test/RToken.test.js index 65a1eab..e4976f3 100644 --- a/test/RToken.test.js +++ b/test/RToken.test.js @@ -1747,53 +1747,120 @@ contract("RToken", accounts => { it("#22 Change allocation strategy multiple times", async () => { let cToken2, compoundAS2, cToken3, compoundAS3; { - // from 0.1 to 0.01 - const result = await createCompoundAllocationStrategy(toWad(.01)); + // from 0.1 (AS1) to 0.01 (AS2) + const result = await createCompoundAllocationStrategy(toWad(".01")); cToken2 = result.cToken; compoundAS2 = result.compoundAS; } { - // from 0.01 to 10 - const result = await createCompoundAllocationStrategy(toWad(10)); + // from 0.01 (AS2) to 10 (AS3) + const result = await createCompoundAllocationStrategy(toWad("10")); cToken3 = result.cToken; compoundAS3 = result.compoundAS; } await web3tx(compoundAS2.transferOwnership, "compoundAS2.transferOwnership")(rToken.address); await web3tx(compoundAS3.transferOwnership, "compoundAS3.transferOwnership")(rToken.address); + // create some reserve in the cToken pool for lending + await web3tx(token.transfer, "token.transfer 100 from customer 1 to customer 2")( + customer2, toWad(100), { + from: customer1 + } + ); + await web3tx(token.approve, "token.approve 100 by customer2")(cToken.address, toWad(100), { + from: customer2 + }); + await web3tx(cToken.mint, "cToken.mint 100 to customer2")(toWad(100), { + from: customer2 + }); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "900.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "100.00000"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(compoundAS.address)), "0.00000"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(customer2)), "100.00000"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .toString(), toWad(1000)); + assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "0.00000"); + + // mint 100 rToken await web3tx(token.approve, "token.approve 100 by customer1")(rToken.address, toWad(100), { from: customer1 }); await web3tx(rToken.mint, "rToken.mint 100 to customer1")( - toWad(100), { + toWad("100"), { from: customer1 }); - assert.equal(wad4human(await token.balanceOf.call(customer1)), "900.00000"); - assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "100.00000"); - assert.equal(wad4human(await cToken.balanceOf.call(compoundAS.address)), "1000.00000"); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "800.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "200.00000"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(compoundAS.address)), "100.00000"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(customer2)), "100.00000"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .toString(), toWad(1000)); assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "100.00000"); assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00000"); + await doBingeBorrowing(); + + // trivial case: transfer the the same allocation strategy + assert.equal((await rToken.savingAssetConversionRate.call()).toString(), toWad(1).toString()); + await web3tx(rToken.changeAllocationStrategy, + "change to the same allocation strategy", { + inLogs: [{ + name: "AllocationStrategyChanged", + args: { + strategy: compoundAS.address, + conversionRate: toWad(1) + } + }] + })( + compoundAS.address, { + from: admin + }); + assert.equal((await rToken.savingAssetConversionRate.call()).toString(), toWad(1).toString()); + assert.equal(wad4human(await token.balanceOf.call(bingeBorrower)), "10.00000"); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "800.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "190.00000"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(compoundAS.address)), "100.00051"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(customer2)), "100.00051"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(bingeBorrower)) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .toString(), toWad(1000)); + assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "100.00000"); + assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00051"); + await web3tx(rToken.changeAllocationStrategy, "change allocation strategy 1st time", { inLogs: [{ name: "AllocationStrategyChanged", args: { strategy: compoundAS2.address, - conversionRate: "99999999999999995" + conversionRate: toWad(".099999490001595991") } }] })( compoundAS2.address, { from: admin }); - assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "0.00000"); - assert.equal(wad4human(await cToken.balanceOf.call(compoundAS.address)), "0.00000"); - assert.equal(wad4human(await token.balanceOf.call(cToken2.address)), "100.00000"); - assert.equal(wad4human(await cToken2.balanceOf.call(compoundAS2.address)), "10000.00000"); - assert.equal(wad4human(await token.balanceOf.call(customer1)), "900.00000"); + assert.equal(wad4human(await token.balanceOf.call(bingeBorrower)), "10.00000"); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "800.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "89.99949"); + assert.equal((await cToken.balanceOfUnderlying.call(compoundAS.address)).toString(), "0"); + assert.equal(wad4human(await cToken.balanceOfUnderlying.call(customer2)), "100.00051"); + assert.equal(wad4human(await token.balanceOf.call(cToken2.address)), "100.00051"); + assert.equal(wad4human(await cToken2.balanceOfUnderlying.call(compoundAS2.address)), "100.00051"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(bingeBorrower)) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .add(await token.balanceOf.call(cToken2.address)) + .toString(), toWad(1000)); assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "100.00000"); - assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00000"); + assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00051"); await web3tx(rToken.changeAllocationStrategy, "change allocation strategy 2nd time", { @@ -1801,30 +1868,62 @@ contract("RToken", accounts => { name: "AllocationStrategyChanged", args: { strategy: compoundAS3.address, - conversionRate: "99999999999999995000" + conversionRate: toWad("99.999490001595991007") } }] })( compoundAS3.address, { from: admin }); - assert.equal(wad4human(await token.balanceOf.call(cToken2.address)), "0.00000"); - assert.equal(wad4human(await cToken2.balanceOf.call(compoundAS2.address)), "0.00000"); - assert.equal(wad4human(await token.balanceOf.call(cToken3.address)), "100.00000"); - assert.equal(wad4human(await cToken3.balanceOf.call(compoundAS3.address)), "10.00000"); - assert.equal(wad4human(await token.balanceOf.call(customer1)), "900.00000"); + assert.equal(wad4human(await token.balanceOf.call(bingeBorrower)), "10.00000"); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "800.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "89.99949"); + assert.equal(wad4human(await token.balanceOf.call(cToken3.address)), "100.00051"); + assert.equal((await cToken.balanceOfUnderlying.call(compoundAS.address)).toString(), "0"); + assert.equal((await cToken2.balanceOfUnderlying.call(compoundAS2.address)).toString(), "0"); + assert.equal(wad4human(await cToken3.balanceOfUnderlying.call(compoundAS3.address)), "100.00051"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(bingeBorrower)) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .add(await token.balanceOf.call(cToken3.address)) + .toString(), toWad(1000)); assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "100.00000"); - assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00000"); + assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "100.00051"); - await web3tx(rToken.redeem, "rToken.redeem 10 to customer1")( - toWad(100), { + + // create some reserve in the cToken3 pool for lending in order to make redeemAll solvent + await web3tx(token.transfer, "token.transfer 100 from customer 1 to customer 2")( + customer2, toWad(100), { from: customer1 - }); - assert.equal(wad4human(await token.balanceOf.call(cToken3.address)), "0.00000"); - assert.equal(wad4human(await cToken3.balanceOf.call(compoundAS3.address)), "0.00000"); - assert.equal(wad4human(await token.balanceOf.call(customer1)), "1000.00000"); + } + ); + await web3tx(token.approve, "token.approve 100 by customer2")(rToken.address, toWad(100), { + from: customer2 + }); + await web3tx(rToken.mint, "rToken.mint 100 to customer2")(toWad(100), { + from: customer2 + }); + await web3tx(rToken.redeemAll, "rToken.redeemAll to customer1")({ + from: customer1 + }); + assert.equal(wad4human(await token.balanceOf.call(bingeBorrower)), "10.00000"); + assert.equal(wad4human(await token.balanceOf.call(cToken.address)), "89.99949"); + assert.equal(wad4human(await token.balanceOf.call(cToken3.address)), "100.00000"); + assert.equal(wad4human(await token.balanceOf.call(customer1)), "800.00051"); + assert.equal((await cToken.balanceOfUnderlying.call(compoundAS.address)).toString(), "0"); + assert.equal((await cToken2.balanceOfUnderlying.call(compoundAS2.address)).toString(), "0"); + assert.equal(wad4human(await cToken3.balanceOfUnderlying.call(compoundAS3.address)), "100.00000"); + assert.equal(toWad(0) + .add(await token.balanceOf.call(bingeBorrower)) + .add(await token.balanceOf.call(customer1)) + .add(await token.balanceOf.call(cToken.address)) + .add(await token.balanceOf.call(cToken3.address)) + .toString(), toWad(1000)); assert.equal(wad4human(await rToken.balanceOf.call(customer1)), "0.00000"); + assert.equal(wad4human(await rToken.balanceOf.call(customer2)), "100.00000"); assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer1)), "0.00000"); + assert.equal(wad4human(await rToken.receivedSavingsOf.call(customer2)), "100.00000"); }); it("#23 rToken change hat test", async () => {