diff --git a/contracts/Foundry.sol b/contracts/Foundry.sol index 4e593b48..94a1acb1 100644 --- a/contracts/Foundry.sol +++ b/contracts/Foundry.sol @@ -243,6 +243,24 @@ contract Foundry is IFoundry, Ownable, Initializable { ); } + function donate(address _meToken, uint256 _assetsDeposited) + external + override + { + Details.MeToken memory meToken_ = meTokenRegistry.getDetails(_meToken); + Details.Hub memory hub_ = hub.getDetails(meToken_.hubId); + require(meToken_.migration == address(0), "meToken resubscribing"); + + IVault vault = IVault(hub_.vault); + address asset = hub_.asset; + + vault.handleDeposit(msg.sender, asset, _assetsDeposited, 0); + + meTokenRegistry.updateBalanceLocked(true, _meToken, _assetsDeposited); + + emit Donate(_meToken, asset, msg.sender, _assetsDeposited); + } + // NOTE: for now this does not include fees function _calculateMeTokensMinted( address _meToken, diff --git a/contracts/interfaces/IFoundry.sol b/contracts/interfaces/IFoundry.sol index 6455ccc7..65f9a317 100644 --- a/contracts/interfaces/IFoundry.sol +++ b/contracts/interfaces/IFoundry.sol @@ -36,6 +36,18 @@ interface IFoundry { uint256 _assetsReturned ); + /// @notice Event of donating to meToken owner + /// @param _meToken address of meToken burned + /// @param _asset address of asset returned + /// @param _donor address donating the asset + /// @param _assetsDeposited amount of assets to c + event Donate( + address _meToken, + address _asset, + address _donor, + uint256 _assetsDeposited + ); + /// @notice Mint a meToken by depositing the underlying asset /// @param _meToken address of meToken to mint /// @param _assetsDeposited amount of assets to deposit @@ -55,4 +67,9 @@ interface IFoundry { uint256 _meTokensBurned, address _recipient ) external; + + /// @notice Donate a meToken's underlying asset to its owner + /// @param _meToken address of meToken to burn + /// @param _assetsDeposited amount of asset to donate + function donate(address _meToken, uint256 _assetsDeposited) external; } diff --git a/contracts/migrations/UniswapSingleTransferMigration.sol b/contracts/migrations/UniswapSingleTransferMigration.sol index d3275835..412fd171 100644 --- a/contracts/migrations/UniswapSingleTransferMigration.sol +++ b/contracts/migrations/UniswapSingleTransferMigration.sol @@ -54,6 +54,12 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { { require(msg.sender == address(meTokenRegistry), "!meTokenRegistry"); + Details.MeToken memory meToken_ = meTokenRegistry.getDetails(_meToken); + Details.Hub memory hub_ = hub.getDetails(meToken_.hubId); + Details.Hub memory targetHub_ = hub.getDetails(meToken_.targetHubId); + + require(hub_.asset != targetHub_.asset, "same asset"); + (uint256 soonest, uint24 fee) = abi.decode( _encodedArgs, (uint256, uint24) diff --git a/test/contracts/Foundry.ts b/test/contracts/Foundry.ts index 8715f557..26ac4150 100644 --- a/test/contracts/Foundry.ts +++ b/test/contracts/Foundry.ts @@ -25,14 +25,14 @@ import { MeToken } from "../../artifacts/types/MeToken"; import { expect } from "chai"; import { UniswapSingleTransferMigration } from "../../artifacts/types/UniswapSingleTransferMigration"; import { hubSetup } from "../utils/hubSetup"; -import { ICurve } from "../../artifacts/types"; +import { ICurve, SameAssetTransferMigration } from "../../artifacts/types"; const setup = async () => { describe("Foundry.sol", () => { let DAI: string; - let DAIWhale: string; - let daiHolder: Signer; + let WETH: string; let dai: ERC20; + let weth: ERC20; let account0: SignerWithAddress; let account1: SignerWithAddress; let account2: SignerWithAddress; @@ -46,6 +46,8 @@ const setup = async () => { let singleAssetVault: SingleAssetVault; let migrationRegistry: MigrationRegistry; let curveRegistry: CurveRegistry; + let encodedCurveDetails: string; + let encodedVaultArgs: string; const hubId = 1; const name = "Carl meToken"; @@ -60,6 +62,7 @@ const setup = async () => { const tokenDeposited = ethers.utils.parseEther( tokenDepositedInETH.toString() ); + const fee = 3000; // TODO: pass in curve arguments to function // TODO: then loop over array of set of curve arguments @@ -72,13 +75,13 @@ const setup = async () => { // weight at 50% linear curve // const reserveWeight = BigNumber.from(MAX_WEIGHT).div(2).toString(); before(async () => { - ({ DAI, DAIWhale } = await getNamedAccounts()); - const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( + ({ DAI, WETH } = await getNamedAccounts()); + encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( ["address"], [DAI] ); // TODO: pass in name of curve to deploy, encodedCurveDetails to general func - const encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( ["uint256", "uint32"], [baseY, reserveWeight] ); @@ -109,9 +112,13 @@ const setup = async () => { // Prefund owner/buyer w/ DAI dai = token; + weth = await getContractAt("ERC20", WETH); await dai .connect(tokenHolder) .transfer(account0.address, amount1.mul(10)); + await weth + .connect(tokenHolder) + .transfer(account0.address, amount1.mul(10)); await dai .connect(tokenHolder) .transfer(account1.address, amount1.mul(10)); @@ -120,6 +127,7 @@ const setup = async () => { .transfer(account2.address, amount1.mul(10)); const max = ethers.constants.MaxUint256; await dai.connect(account0).approve(singleAssetVault.address, max); + await weth.connect(account0).approve(singleAssetVault.address, max); await dai.connect(account1).approve(singleAssetVault.address, max); await dai.connect(account2).approve(singleAssetVault.address, max); await dai.connect(account1).approve(meTokenRegistry.address, max); @@ -1072,6 +1080,232 @@ const setup = async () => { await meToken.balanceOf(account2.address) ); }); + after(async () => { + const oldDetails = await hub.getDetails(hubId); + await mineBlock(oldDetails.endTime.toNumber() + 2); + const block = await ethers.provider.getBlock("latest"); + expect(oldDetails.endTime).to.be.lt(block.timestamp); + + await hub.finishUpdate(hubId); + const newDetails = await hub.getDetails(hubId); + expect(newDetails.updating).to.be.equal(false); + }); + }); + describe("donate with same asset migration", () => { + let migration: SameAssetTransferMigration; + before(async () => { + await hub.register( + account0.address, + DAI, + singleAssetVault.address, + _curve.address, + refundRatio, + encodedCurveDetails, + encodedVaultArgs + ); + migration = await deploy( + "SameAssetTransferMigration", + undefined, + account0.address, + foundry.address, + hub.address, + meTokenRegistry.address, + migrationRegistry.address + ); + + await migrationRegistry.approve( + singleAssetVault.address, + singleAssetVault.address, + migration.address + ); + + const encodedMigrationArgs = "0x"; + + await meTokenRegistry + .connect(account2) + .initResubscribe( + meToken.address, + 2, + migration.address, + encodedMigrationArgs + ); + expect( + (await meTokenRegistry.getDetails(meToken.address)).migration + ).to.equal(migration.address); + }); + it("should revert when meToken is resubscribing", async () => { + await expect(foundry.donate(meToken.address, 10)).to.be.revertedWith( + "meToken resubscribing" + ); + }); + it("should be able to donate", async () => { + const meTokenRegistryDetails = await meTokenRegistry.getDetails( + meToken.address + ); + await mineBlock(meTokenRegistryDetails.endTime.toNumber() + 2); + const block = await ethers.provider.getBlock("latest"); + expect(meTokenRegistryDetails.endTime).to.be.lt(block.timestamp); + await meTokenRegistry.finishResubscribe(meToken.address); + + const oldVaultBalance = await dai.balanceOf(singleAssetVault.address); + const oldAccountBalance = await dai.balanceOf(account0.address); + const oldMeTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const oldAccruedFee = await singleAssetVault.accruedFees(dai.address); + + const assetsDeposited = 10; + const tx = await foundry.donate(meToken.address, assetsDeposited); + + await expect(tx) + .to.emit(foundry, "Donate") + .withArgs( + meToken.address, + dai.address, + account0.address, + assetsDeposited + ) + .to.emit(singleAssetVault, "HandleDeposit") + .withArgs(account0.address, dai.address, assetsDeposited, 0) + .to.emit(meTokenRegistry, "UpdateBalanceLocked") + .withArgs(true, meToken.address, assetsDeposited); + + const newMeTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const newVaultBalance = await dai.balanceOf(singleAssetVault.address); + const newAccountBalance = await dai.balanceOf(account0.address); + const newAccruedFee = await singleAssetVault.accruedFees(dai.address); + + expect(oldMeTokenDetails.balanceLocked.add(assetsDeposited)).to.equal( + newMeTokenDetails.balanceLocked + ); + expect(oldMeTokenDetails.balancePooled).to.equal( + newMeTokenDetails.balancePooled + ); + expect(oldVaultBalance.add(assetsDeposited)).to.equal(newVaultBalance); + expect(oldAccountBalance.sub(assetsDeposited)).to.equal( + newAccountBalance + ); + expect(oldAccruedFee).to.equal(newAccruedFee); + }); + }); + + describe("donate with same UniswapSingleTransfer migration", () => { + let migration: UniswapSingleTransferMigration; + before(async () => { + await hub.register( + account0.address, + WETH, + singleAssetVault.address, + _curve.address, + refundRatio, + encodedCurveDetails, + encodedVaultArgs + ); + migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, + foundry.address, + hub.address, + meTokenRegistry.address, + migrationRegistry.address + ); + + await migrationRegistry.approve( + singleAssetVault.address, + singleAssetVault.address, + migration.address + ); + + let block = await ethers.provider.getBlock("latest"); + const earliestSwapTime = block.timestamp + 600 * 60; // 10h in future + const encodedMigrationArgs = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint24"], + [earliestSwapTime, fee] + ); + + await meTokenRegistry + .connect(account2) + .initResubscribe( + meToken.address, + 3, + migration.address, + encodedMigrationArgs + ); + expect( + (await meTokenRegistry.getDetails(meToken.address)).migration + ).to.equal(migration.address); + const migrationDetails = await migration.getDetails(meToken.address); + await mineBlock(migrationDetails.soonest.toNumber() + 2); + + block = await ethers.provider.getBlock("latest"); + expect(migrationDetails.soonest).to.be.lt(block.timestamp); + }); + it("should revert when meToken is resubscribing", async () => { + await expect(foundry.donate(meToken.address, 10)).to.be.revertedWith( + "meToken resubscribing" + ); + }); + it("should be able to donate", async () => { + await meTokenRegistry.finishResubscribe(meToken.address); + + const oldDAIVaultBalance = await dai.balanceOf( + singleAssetVault.address + ); + const oldWETHVaultBalance = await weth.balanceOf( + singleAssetVault.address + ); + const oldAccountBalance = await weth.balanceOf(account0.address); + const oldMeTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const oldAccruedFee = await singleAssetVault.accruedFees(weth.address); + + const assetsDeposited = 10; + const tx = await foundry.donate(meToken.address, assetsDeposited); + + await expect(tx) + .to.emit(foundry, "Donate") + .withArgs( + meToken.address, + weth.address, + account0.address, + assetsDeposited + ) + .to.emit(singleAssetVault, "HandleDeposit") + .withArgs(account0.address, weth.address, assetsDeposited, 0) + .to.emit(meTokenRegistry, "UpdateBalanceLocked") + .withArgs(true, meToken.address, assetsDeposited); + + const newMeTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const newDAIVaultBalance = await dai.balanceOf( + singleAssetVault.address + ); + const newWETHVaultBalance = await weth.balanceOf( + singleAssetVault.address + ); + const newAccountBalance = await weth.balanceOf(account0.address); + const newAccruedFee = await singleAssetVault.accruedFees(weth.address); + + expect(oldMeTokenDetails.balanceLocked.add(assetsDeposited)).to.equal( + newMeTokenDetails.balanceLocked + ); + expect(oldMeTokenDetails.balancePooled).to.equal( + newMeTokenDetails.balancePooled + ); + expect(oldDAIVaultBalance).to.equal(newDAIVaultBalance); + expect(oldWETHVaultBalance.add(assetsDeposited)).to.equal( + newWETHVaultBalance + ); + expect(oldAccountBalance.sub(assetsDeposited)).to.equal( + newAccountBalance + ); + expect(oldAccruedFee).to.equal(newAccruedFee); + }); }); }); }; diff --git a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts index 870ca4fc..049d127f 100644 --- a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts +++ b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts @@ -114,11 +114,11 @@ const setup = async () => { // Pre-load owner and buyer w/ DAI await dai .connect(tokenHolder) - .transfer(account2.address, ethers.utils.parseEther("500")); + .transfer(account2.address, ethers.utils.parseEther("250")); await weth .connect(tokenHolder) - .transfer(account2.address, ethers.utils.parseEther("500")); + .transfer(account2.address, ethers.utils.parseEther("250")); // Create meToken and subscribe to Hub1 await meTokenRegistry @@ -155,7 +155,7 @@ const setup = async () => { migration.address, encodedMigrationArgs ); - tokenDepositedInETH = 100; + tokenDepositedInETH = 50; tokenDeposited = ethers.utils.parseEther(tokenDepositedInETH.toString()); await dai .connect(account2)