From ca5c2abab07a51869b42d09496672bad265863d0 Mon Sep 17 00:00:00 2001 From: Richard Guan Date: Tue, 5 Apr 2022 11:54:31 -0600 Subject: [PATCH] feat(adapter): Add RGT to Tribe migration adapter [SIM-52] (#188) * add RGT to Tribe migration adapter --- .../mocks/external/TribePegExchangerMock.sol | 54 ++++++ .../wrap/RgtMigrationWrapAdapter.sol | 99 ++++++++++ .../rgtMigrationWrapModule.spec.ts | 174 ++++++++++++++++++ .../wrap/rgtMigrationWrapAdapter.spec.ts | 114 ++++++++++++ utils/contracts/index.ts | 2 + utils/deploys/deployAdapters.ts | 7 +- utils/deploys/deployMocks.ts | 6 + 7 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/external/TribePegExchangerMock.sol create mode 100644 contracts/protocol/integration/wrap/RgtMigrationWrapAdapter.sol create mode 100644 test/integration/rgtMigrationWrapModule.spec.ts create mode 100644 test/protocol/integration/wrap/rgtMigrationWrapAdapter.spec.ts diff --git a/contracts/mocks/external/TribePegExchangerMock.sol b/contracts/mocks/external/TribePegExchangerMock.sol new file mode 100644 index 000000000..c989b90cb --- /dev/null +++ b/contracts/mocks/external/TribePegExchangerMock.sol @@ -0,0 +1,54 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +/** + * @title Contract to exchange RGT with TRIBE post-merger + */ +contract TribePegExchangerMock { + using SafeERC20 for IERC20; + + /// @notice the multiplier applied to RGT before converting to TRIBE scaled up by 1e9 + uint256 public constant exchangeRate = 26705673430; // 26.7 TRIBE / RGT + /// @notice the granularity of the exchange rate + uint256 public constant scalar = 1e9; + + event Exchange(address indexed from, uint256 amountIn, uint256 amountOut); + + address public immutable rgt; + address public immutable tribe; + + constructor(address _rgt, address _tribe) public { + rgt = _rgt; + tribe = _tribe; + } + + /// @notice call to exchange held RGT with TRIBE + /// @param amount the amount to exchange + /// Mirrors the real contract without the permission state checks. + function exchange(uint256 amount) public { + uint256 tribeOut = amount * exchangeRate / scalar; + IERC20(rgt).safeTransferFrom(msg.sender, address(this), amount); + IERC20(tribe).safeTransfer(msg.sender, tribeOut); + emit Exchange(msg.sender, amount, tribeOut); + } +} diff --git a/contracts/protocol/integration/wrap/RgtMigrationWrapAdapter.sol b/contracts/protocol/integration/wrap/RgtMigrationWrapAdapter.sol new file mode 100644 index 000000000..a0c7aa51f --- /dev/null +++ b/contracts/protocol/integration/wrap/RgtMigrationWrapAdapter.sol @@ -0,0 +1,99 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +/** + * @title RgtMigrationWrapAdater + * @author FlattestWhite + * + * Wrap adapter for one time token migration that returns data for wrapping RGT into TRIBE. + * Note: RGT can not be unwrapped into TRIBE, because migration can not be reversed. + */ +contract RgtMigrationWrapAdapter { + + /* ============ State Variables ============ */ + + address public immutable pegExchanger; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _pegExchanger Address of PegExchanger contract + */ + constructor( + address _pegExchanger + ) + public + { + pegExchanger = _pegExchanger; + } + + /* ============ External Getter Functions ============ */ + + /** + * Generates the calldata to migrate RGT tokens to TRIBE tokens. + * + * @param _underlyingUnits Total quantity of underlying units to wrap + * + * @return address Target contract address + * @return uint256 Total quantity of underlying units (if underlying is ETH) + * @return bytes Wrap calldata + */ + function getWrapCallData( + address /* _underlyingToken */, + address /* _wrappedToken */, + uint256 _underlyingUnits + ) + external + view + returns (address, uint256, bytes memory) + { + // exchange(uint256 amount) + bytes memory callData = abi.encodeWithSignature("exchange(uint256)", _underlyingUnits); + + return (pegExchanger, 0, callData); + } + + /** + * This function will revert, since migration cannot be reversed. + */ + function getUnwrapCallData( + address /* _underlyingToken */, + address /* _wrappedToken */, + uint256 /* _wrappedTokenUnits */ + ) + external + pure + returns (address, uint256, bytes memory) + { + revert("RGT migration cannot be reversed"); + } + + /** + * Returns the address to approve source tokens for wrapping. + * + * @return address Address of the contract to approve tokens to + */ + function getSpenderAddress(address /* _underlyingToken */, address /* _wrappedToken */) external view returns(address) { + return pegExchanger; + } +} diff --git a/test/integration/rgtMigrationWrapModule.spec.ts b/test/integration/rgtMigrationWrapModule.spec.ts new file mode 100644 index 000000000..db3b7c026 --- /dev/null +++ b/test/integration/rgtMigrationWrapModule.spec.ts @@ -0,0 +1,174 @@ +import "module-alias/register"; +import { BigNumber } from "ethers"; + +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, ZERO } from "@utils/constants"; +import { RgtMigrationWrapAdapter, SetToken, StandardTokenMock, TribePegExchangerMock, WrapModule } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getSystemFixture, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("rgtMigrationWrapModule", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let wrapModule: WrapModule; + + let rgtToken: StandardTokenMock; + let tribeToken: StandardTokenMock; + let pegExchanger: TribePegExchangerMock; + let adapter: RgtMigrationWrapAdapter; + + const rgtMigrationWrapAdapterIntegrationName: string = "RGT_MIGRATION_WRAPPER"; + const exchangeRate = 26705673430; + const scalar = 1e9; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + // System setup + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + // WrapModule setup + wrapModule = await deployer.modules.deployWrapModule(setup.controller.address, setup.weth.address); + await setup.controller.addModule(wrapModule.address); + + rgtToken = await deployer.mocks.deployTokenMock(owner.address); + tribeToken = await deployer.mocks.deployTokenMock(owner.address); + + // RgtMigrationWrapV2Adapter setup + pegExchanger = await deployer.mocks.deployTribePegExchangerMock(rgtToken.address, tribeToken.address); + adapter = await deployer.adapters.deployRgtMigrationWrapAdapter(pegExchanger.address); + + await setup.integrationRegistry.addIntegration(wrapModule.address, rgtMigrationWrapAdapterIntegrationName, adapter.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when a SetToken has been deployed and issued", async () => { + let setToken: SetToken; + let setTokensIssued: BigNumber; + + before(async () => { + setToken = await setup.createSetToken( + [rgtToken.address], + [BigNumber.from(10 ** 8)], + [setup.issuanceModule.address, wrapModule.address] + ); + + // Initialize modules + await setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + await wrapModule.initialize(setToken.address); + + // Issue some Sets + setTokensIssued = ether(10); + await rgtToken.approve(setup.issuanceModule.address, BigNumber.from(10 ** 9)); + await setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = rgtToken.address; + subjectWrappedToken = tribeToken.address; + subjectUnderlyingUnits = BigNumber.from(10 ** 8); + subjectIntegrationName = rgtMigrationWrapAdapterIntegrationName; + subjectCaller = owner; + + await tribeToken.mint(pegExchanger.address, BigNumber.from(10 ** 9).mul(exchangeRate).div(scalar)); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName + ); + } + + it("should convert underlying balance of RGT tokens to TRIBE tokens * multiplier", async () => { + const previousRgtTokenBalance = await rgtToken.balanceOf(setToken.address); + const previousTribeTokenBalance = await tribeToken.balanceOf(setToken.address); + expect(previousRgtTokenBalance).to.eq(BigNumber.from(10 ** 9)); + expect(previousTribeTokenBalance).to.eq(ZERO); + + await subject(); + + const rgtTokenBalance = await rgtToken.balanceOf(setToken.address); + const tribeTokenBalance = await tribeToken.balanceOf(setToken.address); + const components = await setToken.getComponents(); + + expect(rgtTokenBalance).to.eq(ZERO); + expect(tribeTokenBalance).to.eq(previousRgtTokenBalance.mul(exchangeRate).div(scalar)); + expect(components.length).to.eq(1); + }); + }); + + describe("#unwrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedUnits: BigNumber; + let subjectIntegrationName: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = rgtToken.address; + subjectWrappedToken = tribeToken.address; + subjectWrappedUnits = BigNumber.from(10 ** 8); + subjectIntegrationName = rgtMigrationWrapAdapterIntegrationName; + subjectCaller = owner; + + await tribeToken.mint(pegExchanger.address, BigNumber.from(10 ** 9).mul(exchangeRate).div(scalar)); + + await wrapModule.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedUnits, + subjectIntegrationName + ); + }); + + async function subject(): Promise { + return wrapModule.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedUnits, + subjectIntegrationName + ); + } + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("RGT migration cannot be reversed"); + }); + }); + }); +}); diff --git a/test/protocol/integration/wrap/rgtMigrationWrapAdapter.spec.ts b/test/protocol/integration/wrap/rgtMigrationWrapAdapter.spec.ts new file mode 100644 index 000000000..7087b07d5 --- /dev/null +++ b/test/protocol/integration/wrap/rgtMigrationWrapAdapter.spec.ts @@ -0,0 +1,114 @@ +import "module-alias/register"; + +import { BigNumber } from "ethers"; +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ZERO } from "@utils/constants"; +import { RgtMigrationWrapAdapter } from "@utils/contracts"; +import { TribePegExchangerMock } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect +} from "@utils/test/index"; + +const expect = getWaffleExpect(); + +describe("RgtMigrationWrapAdapter", () => { + let owner: Account; + let deployer: DeployHelper; + let pegExchanger: TribePegExchangerMock; + let rgtMigrationWrapAdapter: RgtMigrationWrapAdapter; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + const rgt = await deployer.mocks.deployTokenMock(owner.address); + const tribe = await deployer.mocks.deployTokenMock(owner.address); + pegExchanger = await deployer.mocks.deployTribePegExchangerMock(rgt.address, tribe.address); + + rgtMigrationWrapAdapter = await deployer.adapters.deployRgtMigrationWrapAdapter(pegExchanger.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectPegExchanger: Address; + + beforeEach(async () => { + subjectPegExchanger = pegExchanger.address; + }); + + async function subject(): Promise { + return deployer.adapters.deployRgtMigrationWrapAdapter(subjectPegExchanger); + } + + it("should have the correct PegExchanger address", async () => { + const deployRgtMigrationWrapAdapter = await subject(); + + const pegExchanger = await deployRgtMigrationWrapAdapter.pegExchanger(); + const expectedPegExchanger = subjectPegExchanger; + + expect(pegExchanger).to.eq(expectedPegExchanger); + }); + }); + + describe("#getSpenderAddress", async () => { + async function subject(): Promise { + return rgtMigrationWrapAdapter.getSpenderAddress(owner.address, owner.address); + } + + it("should return the correct spender address", async () => { + const spender = await subject(); + + expect(spender).to.eq(pegExchanger.address); + }); + }); + + describe("#getWrapCallData", async () => { + let subjectUnderlyingUnits: BigNumber; + + beforeEach(async () => { + subjectUnderlyingUnits = ether(2); + }); + + async function subject(): Promise { + return rgtMigrationWrapAdapter.getWrapCallData(owner.address, owner.address, subjectUnderlyingUnits); + } + + it("should return correct data for valid pair", async () => { + const [targetAddress, ethValue, callData] = await subject(); + + const expectedCallData = pegExchanger.interface.encodeFunctionData("exchange", [subjectUnderlyingUnits]); + + expect(targetAddress).to.eq(pegExchanger.address); + expect(ethValue).to.eq(ZERO); + expect(callData).to.eq(expectedCallData); + }); + }); + + describe("#getUnwrapCallData", async () => { + let subjectWrappedTokenUnits: BigNumber; + + beforeEach(async () => { + subjectWrappedTokenUnits = ether(2); + }); + + async function subject(): Promise { + return rgtMigrationWrapAdapter.getUnwrapCallData(owner.address, owner.address, subjectWrappedTokenUnits); + } + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("RGT migration cannot be reversed"); + }); + }); + +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 181846adb..609d1d188 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -76,6 +76,7 @@ export { PreciseUnitMathMock } from "../../typechain/PreciseUnitMathMock"; export { PriceOracle } from "../../typechain/PriceOracle"; export { ProtocolViewer } from "../../typechain/ProtocolViewer"; export { ResourceIdentifierMock } from "../../typechain/ResourceIdentifierMock"; +export { RgtMigrationWrapAdapter } from "../../typechain/RgtMigrationWrapAdapter"; export { SetToken } from "../../typechain/SetToken"; export { SetTokenAccessibleMock } from "../../typechain/SetTokenAccessibleMock"; export { SetTokenCreator } from "../../typechain/SetTokenCreator"; @@ -94,6 +95,7 @@ export { StreamingFeeModule } from "../../typechain/StreamingFeeModule"; export { SynthetixExchangeAdapter } from "../../typechain/SynthetixExchangeAdapter"; export { SynthetixExchangerMock } from "../../typechain/SynthetixExchangerMock"; export { SynthMock } from "../../typechain/SynthMock"; +export { TribePegExchangerMock } from "../../typechain/TribePegExchangerMock"; export { TradeModule } from "../../typechain/TradeModule"; export { TradeAdapterMock } from "../../typechain/TradeAdapterMock"; export { Uint256ArrayUtilsMock } from "../../typechain/Uint256ArrayUtilsMock"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index 6621f03db..14abf2524 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -25,6 +25,7 @@ import { SynthetixExchangeAdapter, CompoundBravoGovernanceAdapter, CompClaimAdapter, + RgtMigrationWrapAdapter, } from "../contracts"; import { Address, Bytes } from "./../types"; @@ -52,7 +53,7 @@ import { SnapshotGovernanceAdapter__factory } from "../../typechain/factories/Sn import { SynthetixExchangeAdapter__factory } from "../../typechain/factories/SynthetixExchangeAdapter__factory"; import { CompoundBravoGovernanceAdapter__factory } from "../../typechain/factories/CompoundBravoGovernanceAdapter__factory"; import { CompClaimAdapter__factory } from "../../typechain"; - +import { RgtMigrationWrapAdapter__factory } from "../../typechain/factories/RgtMigrationWrapAdapter__factory"; export default class DeployAdapters { private _deployerSigner: Signer; @@ -121,6 +122,10 @@ export default class DeployAdapters { return await new CurveStakingAdapter__factory(this._deployerSigner).deploy(gaugeController); } + public async deployRgtMigrationWrapAdapter(pegExchanger: Address): Promise { + return await new RgtMigrationWrapAdapter__factory(this._deployerSigner).deploy(pegExchanger); + } + public async deployUniswapPairPriceAdapter( controller: Address, uniswapFactory: Address, diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 8d329161f..9c858447c 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -46,6 +46,7 @@ import { StringArrayUtilsMock, SynthMock, SynthetixExchangerMock, + TribePegExchangerMock, Uint256ArrayUtilsMock, WrapAdapterMock, WrapV2AdapterMock, @@ -100,6 +101,7 @@ import { StakingAdapterMock__factory } from "../../typechain/factories/StakingAd import { StandardTokenMock__factory } from "../../typechain/factories/StandardTokenMock__factory"; import { StandardTokenWithRoundingErrorMock__factory } from "../../typechain/factories/StandardTokenWithRoundingErrorMock__factory"; import { StandardTokenWithFeeMock__factory } from "../../typechain/factories/StandardTokenWithFeeMock__factory"; +import { TribePegExchangerMock__factory } from "../../typechain/factories/TribePegExchangerMock__factory"; import { TradeAdapterMock__factory } from "../../typechain/factories/TradeAdapterMock__factory"; import { Uint256ArrayUtilsMock__factory } from "../../typechain/factories/Uint256ArrayUtilsMock__factory"; import { WrapAdapterMock__factory } from "../../typechain/factories/WrapAdapterMock__factory"; @@ -447,6 +449,10 @@ export default class DeployMocks { return await new ChainlinkAggregatorMock__factory(this._deployerSigner).deploy(decimals); } + public async deployTribePegExchangerMock(rgt: Address, tribe: Address): Promise { + return await new TribePegExchangerMock__factory(this._deployerSigner).deploy(rgt, tribe); + } + public async deployStringArrayUtilsMock(): Promise { return await new StringArrayUtilsMock__factory(this._deployerSigner).deploy(); }