From ab527411e9c7f8ff7750794bfcc726659a589d18 Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 30 Aug 2024 18:20:12 -0500 Subject: [PATCH 01/68] feat: skeleton for GsmConverter buyAsset --- .../facilitators/gsm/GsmConverter.sol | 86 +++++++++++++++++++ .../circle/interfaces/IRedemption.sol | 42 +++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/contracts/facilitators/gsm/GsmConverter.sol create mode 100644 src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol diff --git a/src/contracts/facilitators/gsm/GsmConverter.sol b/src/contracts/facilitators/gsm/GsmConverter.sol new file mode 100644 index 00000000..fb6a76f0 --- /dev/null +++ b/src/contracts/facilitators/gsm/GsmConverter.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +// import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +// import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +// import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +// import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; +// import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; +// import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +// import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; +// import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; +// import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; +// import {IGsmFeeStrategy} from './feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IGsm} from './interfaces/IGsm.sol'; +import {IRedemption} from './dependencies/circle/interfaces/IRedemption.sol'; + +/** + * @title GsmConverter + * @author Aave + * @notice GHO Stability Module. It provides buy/sell facilities to go to/from an underlying asset to/from GHO. + * @dev To be covered by a proxy contract. + */ +contract GsmConverter { + using GPv2SafeERC20 for IERC20; + + address public immutable GSM; + address public immutable GHO_TOKEN; + address public immutable REDEEMABLE_ASSET; // BUIDL token + address public immutable REDEEMED_ASSET; // USDC token + address public immutable REDEMPTION; // redeem BUIDL to USDC + + event BuyAssetThroughRedemption( + address indexed originator, + address indexed receiver, + uint256 redemptionAssetAmount, + uint256 ghoSold + ); + + /** + * @dev Constructor + * @param ghoToken The address of the GHO token contract + * @param underlyingAsset The address of the collateral asset + * @param priceStrategy The address of the price strategy + */ + constructor(address gsm, address redemption, address redeemableAsset, address redeemedAsset) { + require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redemption != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + + GSM = gsm; + REDEMPTION = redemption; + REDEEMABLE_ASSET = redeemableAsset; + REDEEMED_ASSET = redeemedAsset; + } + + // TODO: + // 1) implement buyAsset (sell GHO -> get USDC) + // - GHO transferFrom user to converter + // - call buyAsset on GSM, get the BUIDL + // - offramp BUIDL to USDC, send USDC to user + // - call redeem on offramp to get BUIDL + // - send USDC to user, safeTransfer + + /** + * @dev + */ + function buyAssetThroughRedemption( + uint256 minAmount, + address receiver + ) external returns (uint256, uint256) { + IGhoToken(IGSM(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); + (uint256 redemptionAssetAmount, uint256 ghoSold) = IGSM(GSM).buyAsset(minAmount, receiver); + IRedemption(REDEMPTION).redeem(redemptionAssetAmount); + IERC20(REDEEMED_ASSET).safeTransfer(receiver, redemptionAssetAmount); + + emit BuyAssetThroughRedemption(msg.sender, receiver, redemptionAssetAmount, ghoSold); + } + + // TODO: + // 2) implement sellAsset (sell USDC -> get GHO) + // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently + // - send BUIDL to GSM, get GHO from GSM + // - send GHO to user, safeTransfer +} diff --git a/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol b/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol new file mode 100644 index 00000000..e79e388e --- /dev/null +++ b/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol @@ -0,0 +1,42 @@ +/** + * Copyright 2024 Circle Internet Financial, LTD. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * 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. + */ + +pragma solidity ^0.8.16; + +/** + * @title IRedemption + */ +interface IRedemption { + /** + * @notice The asset being redeemed. + * @return The address of the asset token. + */ + function asset() external view returns (address); + + /** + * @notice The liquidity token that the asset is being redeemed for. + * @return The address of the liquidity token. + */ + function liquidity() external view returns (address); + + /** + * @notice Redeems an amount of asset for liquidity + * @param amount The amount of the asset token to redeem + */ + function redeem(uint256 amount) external; +} From b1021361c10d41f74af8307daaaf75b046ed7527 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 08:46:01 -0500 Subject: [PATCH 02/68] fix: resolve compilation issues --- .../facilitators/gsm/GsmConverter.sol | 37 +++++++++++-------- .../circle/interfaces/IRedemption.sol | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/contracts/facilitators/gsm/GsmConverter.sol b/src/contracts/facilitators/gsm/GsmConverter.sol index fb6a76f0..ceaac273 100644 --- a/src/contracts/facilitators/gsm/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/GsmConverter.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.10; // import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; -// import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; // import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; // import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; // import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; // import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; // import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; -// import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; +import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; // import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; // import {IGsmFeeStrategy} from './feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IGsm} from './interfaces/IGsm.sol'; @@ -25,10 +25,10 @@ contract GsmConverter { using GPv2SafeERC20 for IERC20; address public immutable GSM; - address public immutable GHO_TOKEN; - address public immutable REDEEMABLE_ASSET; // BUIDL token - address public immutable REDEEMED_ASSET; // USDC token - address public immutable REDEMPTION; // redeem BUIDL to USDC + // address public immutable GHO_TOKEN; + address public immutable REDEEMABLE_ASSET; + address public immutable REDEEMED_ASSET; + address public immutable REDEMPTION_CONTRACT; event BuyAssetThroughRedemption( address indexed originator, @@ -39,18 +39,24 @@ contract GsmConverter { /** * @dev Constructor - * @param ghoToken The address of the GHO token contract - * @param underlyingAsset The address of the collateral asset - * @param priceStrategy The address of the price strategy + * @param gsm The address of the GSM contract associated with conversion + * @param redemptionContract The address of the + * @param redeemableAsset The address of the + * @param redeemedAsset The address of the */ - constructor(address gsm, address redemption, address redeemableAsset, address redeemedAsset) { + constructor( + address gsm, + address redemptionContract, + address redeemableAsset, + address redeemedAsset + ) { require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(redemption != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); GSM = gsm; - REDEMPTION = redemption; + REDEMPTION_CONTRACT = redemptionContract; REDEEMABLE_ASSET = redeemableAsset; REDEEMED_ASSET = redeemedAsset; } @@ -62,6 +68,7 @@ contract GsmConverter { // - offramp BUIDL to USDC, send USDC to user // - call redeem on offramp to get BUIDL // - send USDC to user, safeTransfer + // - do we need to use multicall? https://docs.openzeppelin.com/contracts/4.x/utilities#multicall /** * @dev @@ -70,9 +77,9 @@ contract GsmConverter { uint256 minAmount, address receiver ) external returns (uint256, uint256) { - IGhoToken(IGSM(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); - (uint256 redemptionAssetAmount, uint256 ghoSold) = IGSM(GSM).buyAsset(minAmount, receiver); - IRedemption(REDEMPTION).redeem(redemptionAssetAmount); + IGhoToken(IGsm(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); + (uint256 redemptionAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, receiver); + IRedemption(REDEMPTION_CONTRACT).redeem(redemptionAssetAmount); IERC20(REDEEMED_ASSET).safeTransfer(receiver, redemptionAssetAmount); emit BuyAssetThroughRedemption(msg.sender, receiver, redemptionAssetAmount, ghoSold); diff --git a/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol b/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol index e79e388e..509ac5a2 100644 --- a/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol +++ b/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol @@ -16,7 +16,7 @@ * limitations under the License. */ -pragma solidity ^0.8.16; +pragma solidity ^0.8.10; /** * @title IRedemption From dd74cc23a898f8577e4dc7b52d22644cc3e82738 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 16:01:33 -0500 Subject: [PATCH 03/68] feat: initialize interface --- .../converter/interfaces/IGsmConverter.sol | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol new file mode 100644 index 00000000..c32fd5fc --- /dev/null +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title IGsmConverter + * @author Aave + * @notice Defines the behaviour of GSM Converters for conversions/redemptions between two non-GHO assets + * @dev + */ +interface IGsmConverter { + /** + * @dev Emitted when a user buys an asset (selling GHO) in the GSM after a redemption + * @param originator The address of the buyer originating the request + * @param receiver The address of the receiver of the underlying asset + * @param redeemableAssetAmount The amount of the redeemable asset converted + * @param ghoAmount The amount of total GHO sold, inclusive of fee + */ + event BuyAssetThroughRedemption( + address indexed originator, + address indexed receiver, + uint256 redeemableAssetAmount, + uint256 ghoAmount + ); + + /** + * @notice Buys the GSM underlying asset in exchange for selling GHO, after asset redemption + * @param minAmount The minimum amount of the underlying asset to buy + * @param receiver Recipient address of the underlying asset being purchased + * @return The amount of underlying asset bought, after asset redemption + * @return The amount of GHO sold by the user + */ + function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256); + + /** + * @notice Sells the GSM underlying asset in exchange for buying GHO, after asset conversion + * @param maxAmount The maximum amount of the underlying asset to sell + * @param receiver Recipient address of the GHO being purchased + * @return The amount of underlying asset sold, after asset conversion + * @return The amount of GHO bought by the user + */ + // function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); + + /** + * @notice Returns the address of the GSM contract associated with the converter + * @return The address of the GSM contract + */ + function GSM() external pure returns (address); + + /** + * @notice Returns the address of the redeemable asset (token) associated with the converter + * @return The address of the redeemable asset + */ + function REDEEMABLE_ASSET() external pure returns (address); + + /** + * @notice Returns the address of the redeemed asset (token) associated with the converter + * @return The address of the redeemed asset + */ + function REDEEMED_ASSET() external pure returns (address); + + /** + * @notice Returns the address of the redemption contract that manages asset redemptions + * @return The address of the redemption contract + */ + function REDEMPTION_CONTRACT() external pure returns (address); +} From 023a6b45f6a30f2a4bfcc461ca29536c0ba79abe Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 16:01:52 -0500 Subject: [PATCH 04/68] refactor: move files to separate directory --- .../facilitators/gsm/GsmConverter.sol | 93 ------------------- .../gsm/converter/GsmConverter.sol | 71 ++++++++++++++ .../circle/{interfaces => }/IRedemption.sol | 0 3 files changed, 71 insertions(+), 93 deletions(-) delete mode 100644 src/contracts/facilitators/gsm/GsmConverter.sol create mode 100644 src/contracts/facilitators/gsm/converter/GsmConverter.sol rename src/contracts/facilitators/gsm/dependencies/circle/{interfaces => }/IRedemption.sol (100%) diff --git a/src/contracts/facilitators/gsm/GsmConverter.sol b/src/contracts/facilitators/gsm/GsmConverter.sol deleted file mode 100644 index ceaac273..00000000 --- a/src/contracts/facilitators/gsm/GsmConverter.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -// import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; -import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; -// import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; -// import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; -// import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; -// import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; -// import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; -import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; -// import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; -// import {IGsmFeeStrategy} from './feeStrategy/interfaces/IGsmFeeStrategy.sol'; -import {IGsm} from './interfaces/IGsm.sol'; -import {IRedemption} from './dependencies/circle/interfaces/IRedemption.sol'; - -/** - * @title GsmConverter - * @author Aave - * @notice GHO Stability Module. It provides buy/sell facilities to go to/from an underlying asset to/from GHO. - * @dev To be covered by a proxy contract. - */ -contract GsmConverter { - using GPv2SafeERC20 for IERC20; - - address public immutable GSM; - // address public immutable GHO_TOKEN; - address public immutable REDEEMABLE_ASSET; - address public immutable REDEEMED_ASSET; - address public immutable REDEMPTION_CONTRACT; - - event BuyAssetThroughRedemption( - address indexed originator, - address indexed receiver, - uint256 redemptionAssetAmount, - uint256 ghoSold - ); - - /** - * @dev Constructor - * @param gsm The address of the GSM contract associated with conversion - * @param redemptionContract The address of the - * @param redeemableAsset The address of the - * @param redeemedAsset The address of the - */ - constructor( - address gsm, - address redemptionContract, - address redeemableAsset, - address redeemedAsset - ) { - require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); - - GSM = gsm; - REDEMPTION_CONTRACT = redemptionContract; - REDEEMABLE_ASSET = redeemableAsset; - REDEEMED_ASSET = redeemedAsset; - } - - // TODO: - // 1) implement buyAsset (sell GHO -> get USDC) - // - GHO transferFrom user to converter - // - call buyAsset on GSM, get the BUIDL - // - offramp BUIDL to USDC, send USDC to user - // - call redeem on offramp to get BUIDL - // - send USDC to user, safeTransfer - // - do we need to use multicall? https://docs.openzeppelin.com/contracts/4.x/utilities#multicall - - /** - * @dev - */ - function buyAssetThroughRedemption( - uint256 minAmount, - address receiver - ) external returns (uint256, uint256) { - IGhoToken(IGsm(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); - (uint256 redemptionAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, receiver); - IRedemption(REDEMPTION_CONTRACT).redeem(redemptionAssetAmount); - IERC20(REDEEMED_ASSET).safeTransfer(receiver, redemptionAssetAmount); - - emit BuyAssetThroughRedemption(msg.sender, receiver, redemptionAssetAmount, ghoSold); - } - - // TODO: - // 2) implement sellAsset (sell USDC -> get GHO) - // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently - // - send BUIDL to GSM, get GHO from GSM - // - send GHO to user, safeTransfer -} diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol new file mode 100644 index 00000000..5293deeb --- /dev/null +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; +import {IGsm} from '../interfaces/IGsm.sol'; +import {IGsmConverter} from './interfaces/IGsmConverter.sol'; +import {IRedemption} from '../dependencies/circle/IRedemption.sol'; + +/** + * @title GsmConverter + * @author Aave + * @notice Converter that facilitates conversions/redemptions of underlying assets. Integrates with GSM to buy/sell to go to/from an underlying asset to/from GHO. + */ +contract GsmConverter is IGsmConverter { + using GPv2SafeERC20 for IERC20; + + /// @inheritdoc IGsmConverter + address public immutable GSM; + + /// @inheritdoc IGsmConverter + address public immutable REDEEMABLE_ASSET; + + /// @inheritdoc IGsmConverter + address public immutable REDEEMED_ASSET; + + /// @inheritdoc IGsmConverter + address public immutable REDEMPTION_CONTRACT; + + /** + * @dev Constructor + * @param gsm The address of the associated GSM contract + * @param redemptionContract The address of the redemption contract associated with the redemption/conversion + * @param redeemableAsset The address of the asset being redeemed + * @param redeemedAsset The address of the asset being received from redemption + */ + constructor( + address gsm, + address redemptionContract, + address redeemableAsset, + address redeemedAsset + ) { + require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + + GSM = gsm; + REDEMPTION_CONTRACT = redemptionContract; + REDEEMABLE_ASSET = redeemableAsset; // BUIDL + REDEEMED_ASSET = redeemedAsset; // USDC + } + + /// @inheritdoc IGsmConverter + function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { + IGhoToken(IGsm(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); + (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, receiver); + IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); + IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); + + emit BuyAssetThroughRedemption(msg.sender, receiver, redeemableAssetAmount, ghoSold); + return (redeemableAssetAmount, ghoSold); + } + + // TODO: + // 2) implement sellAsset (sell USDC -> get GHO) + // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently + // - send BUIDL to GSM, get GHO from GSM + // - send GHO to user, safeTransfer +} diff --git a/src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol b/src/contracts/facilitators/gsm/dependencies/circle/IRedemption.sol similarity index 100% rename from src/contracts/facilitators/gsm/dependencies/circle/interfaces/IRedemption.sol rename to src/contracts/facilitators/gsm/dependencies/circle/IRedemption.sol From 750eb61670192731ff2d0adff5b57e4e721b2938 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 16:14:26 -0500 Subject: [PATCH 05/68] fix: resolve interface function visibility --- .../gsm/converter/interfaces/IGsmConverter.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index c32fd5fc..e7bfcfb6 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -44,23 +44,23 @@ interface IGsmConverter { * @notice Returns the address of the GSM contract associated with the converter * @return The address of the GSM contract */ - function GSM() external pure returns (address); + function GSM() external view returns (address); /** * @notice Returns the address of the redeemable asset (token) associated with the converter * @return The address of the redeemable asset */ - function REDEEMABLE_ASSET() external pure returns (address); + function REDEEMABLE_ASSET() external view returns (address); /** * @notice Returns the address of the redeemed asset (token) associated with the converter * @return The address of the redeemed asset */ - function REDEEMED_ASSET() external pure returns (address); + function REDEEMED_ASSET() external view returns (address); /** * @notice Returns the address of the redemption contract that manages asset redemptions * @return The address of the redemption contract */ - function REDEMPTION_CONTRACT() external pure returns (address); + function REDEMPTION_CONTRACT() external view returns (address); } From 038e3bee26c210e90e416421486eb50ef96daf03 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 16:15:57 -0500 Subject: [PATCH 06/68] test: initialize constructor unit test --- src/test/TestGhoBase.t.sol | 12 ++++++ src/test/TestGsmConverter.t.sol | 38 +++++++++++++++++++ src/test/mocks/MockRedemption.sol | 63 +++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/test/TestGsmConverter.t.sol create mode 100644 src/test/mocks/MockRedemption.sol diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index e85097cc..95c589fb 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -29,6 +29,7 @@ import {MockUpgradeable} from './mocks/MockUpgradeable.sol'; import {PriceOracle} from '@aave/core-v3/contracts/mocks/oracle/PriceOracle.sol'; import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/TestnetERC20.sol'; import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; +import {MockRedemption} from './mocks/MockRedemption.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -78,6 +79,7 @@ import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedF import {SampleLiquidator} from '../contracts/facilitators/gsm/misc/SampleLiquidator.sol'; import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFreezer.sol'; import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; +import {GsmConverter} from '../contracts/facilitators/gsm/converter/GsmConverter.sol'; contract TestGhoBase is Test, Constants, Events { using WadRayMath for uint256; @@ -102,11 +104,13 @@ contract TestGhoBase is Test, Constants, Events { TestnetERC20 AAVE_TOKEN; IStakedAaveV3 STK_TOKEN; TestnetERC20 USDC_TOKEN; + TestnetERC20 BUIDL_TOKEN; MockERC4626 USDC_4626_TOKEN; MockPool POOL; MockAclManager ACL_MANAGER; MockAddressesProvider PROVIDER; MockConfigurator CONFIGURATOR; + MockRedemption REDEMPTION; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -174,6 +178,12 @@ contract TestGhoBase is Test, Constants, Events { ); STK_TOKEN = IStakedAaveV3(address(stkAaveProxy)); USDC_TOKEN = new TestnetERC20('USD Coin', 'USDC', 6, FAUCET); + BUIDL_TOKEN = new TestnetERC20( + 'BlackRock USD Institutional Digital Liquidity Fund', + 'BUIDL', + 6, + FAUCET + ); USDC_4626_TOKEN = new MockERC4626('USD Coin 4626', '4626', address(USDC_TOKEN)); address ghoTokenAddress = address(GHO_TOKEN); address discountToken = address(STK_TOKEN); @@ -306,6 +316,8 @@ contract TestGhoBase is Test, Constants, Events { controlledFacilitators[1] = address(GHO_GSM); vm.prank(SHORT_EXECUTOR); GHO_STEWARD_V2.setControlledFacilitator(controlledFacilitators, true); + + REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); } function ghoFaucet(address to, uint256 amount) public { diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol new file mode 100644 index 00000000..523e8c6f --- /dev/null +++ b/src/test/TestGsmConverter.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestGsmConverter is TestGhoBase { + // using PercentageMath for uint256; + // using PercentageMath for uint128; + + function setUp() public { + // (gsmSignerAddr, gsmSignerKey) = makeAddrAndKey('gsmSigner'); + } + + function testConstructor() public { + GsmConverter gsmConverter = new GsmConverter( + address(GHO_GSM), + address(REDEMPTION), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + assertEq(gsmConverter.GSM(), address(GHO_GSM), 'Unexpected GSM address'); + assertEq( + gsmConverter.REDEMPTION_CONTRACT(), + address(REDEMPTION), + 'Unexpected redemption contract address' + ); + assertEq( + gsmConverter.REDEEMABLE_ASSET(), + address(BUIDL_TOKEN), + 'Unexpected redeemable asset address' + ); + assertEq( + gsmConverter.REDEEMED_ASSET(), + address(USDC_TOKEN), + 'Unexpected redeemed asset address' + ); + } +} diff --git a/src/test/mocks/MockRedemption.sol b/src/test/mocks/MockRedemption.sol new file mode 100644 index 00000000..ecb60ff3 --- /dev/null +++ b/src/test/mocks/MockRedemption.sol @@ -0,0 +1,63 @@ +/** + * Copyright 2024 Circle Internet Financial, LTD. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * 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. + */ + +pragma solidity ^0.8.10; + +import {IRedemption} from '../../contracts/facilitators/gsm/dependencies/circle/IRedemption.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockRedemption + * @dev Asset token is ERC20-compatible + * @dev Liquidity token is ERC20-compatible + */ +contract MockRedemption is IRedemption { + using SafeERC20 for IERC20; + + /** + * @inheritdoc IRedemption + */ + address public immutable asset; + + /** + * @inheritdoc IRedemption + */ + address public immutable liquidity; + + /** + * @param _asset Address of asset token + * @param _liquidity Address of liquidity token + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @inheritdoc IRedemption + */ + function redeem(uint256 amount) external { + // Intentionally left blank. + } +} From c574b35dbcf2d13dbff14b1bf0c7d2f93190b4c8 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 4 Sep 2024 16:28:41 -0500 Subject: [PATCH 07/68] test: testRevertConstructorZeroAddressParams; --- src/test/TestGsmConverter.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 523e8c6f..f44aad18 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -35,4 +35,18 @@ contract TestGsmConverter is TestGhoBase { 'Unexpected redeemed asset address' ); } + + function testRevertConstructorZeroAddressParams() public { + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter(address(0), address(REDEMPTION), address(BUIDL_TOKEN), address(USDC_TOKEN)); + + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter(address(GHO_GSM), address(0), address(BUIDL_TOKEN), address(USDC_TOKEN)); + + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter(address(GHO_GSM), address(REDEMPTION), address(0), address(USDC_TOKEN)); + + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter(address(GHO_GSM), address(REDEMPTION), address(BUIDL_TOKEN), address(0)); + } } From 56ab408f64b92b2256a921dd8c3ffdef20c9a22f Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 10:50:16 -0500 Subject: [PATCH 08/68] test: initialize BUIDL-specific test GSM --- src/test/TestGhoBase.t.sol | 32 +++++++++++++++++++-- src/test/TestGsmConverter.t.sol | 50 +++++++++++++++++++++++++++------ src/test/helpers/Constants.sol | 2 ++ 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 95c589fb..738f6891 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -110,7 +110,7 @@ contract TestGhoBase is Test, Constants, Events { MockAclManager ACL_MANAGER; MockAddressesProvider PROVIDER; MockConfigurator CONFIGURATOR; - MockRedemption REDEMPTION; + MockRedemption BUIDL_USDC_REDEMPTION; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -121,7 +121,10 @@ contract TestGhoBase is Test, Constants, Events { MockFlashBorrower FLASH_BORROWER; Gsm GHO_GSM; Gsm4626 GHO_GSM_4626; + Gsm GHO_BUIDL_GSM; + GsmConverter GSM_CONVERTER; FixedPriceStrategy GHO_GSM_FIXED_PRICE_STRATEGY; + FixedPriceStrategy GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY; FixedPriceStrategy4626 GHO_GSM_4626_FIXED_PRICE_STRATEGY; FixedFeeStrategy GHO_GSM_FIXED_FEE_STRATEGY; SampleLiquidator GHO_GSM_LAST_RESORT_LIQUIDATOR; @@ -256,6 +259,11 @@ contract TestGhoBase is Test, Constants, Events { address(USDC_4626_TOKEN), 6 ); + GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY = new FixedPriceStrategy( + DEFAULT_FIXED_PRICE, + address(BUIDL_TOKEN), + 6 + ); GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM_LAST_RESORT_LIQUIDATOR = new SampleLiquidator(); GHO_GSM_SWAP_FREEZER = new SampleSwapFreezer(); @@ -279,6 +287,19 @@ contract TestGhoBase is Test, Constants, Events { ); GHO_GSM_4626.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + Gsm buidlGsm = new Gsm( + address(GHO_TOKEN), + address(BUIDL_TOKEN), + address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) + ); + AdminUpgradeabilityProxy buidlGsmProxy = new AdminUpgradeabilityProxy( + address(buidlGsm), + SHORT_EXECUTOR, + '' + ); + GHO_BUIDL_GSM = Gsm(address(buidlGsmProxy)); + GHO_BUIDL_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); GHO_GSM_4626.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); @@ -290,6 +311,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_TOKEN.addFacilitator(address(GHO_GSM), 'GSM Facilitator', DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(address(GHO_GSM_4626), 'GSM 4626 Facilitator', DEFAULT_CAPACITY); + GHO_TOKEN.addFacilitator(address(GHO_BUIDL_GSM), 'GSM BUIDL Facilitator', DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); @@ -317,7 +339,13 @@ contract TestGhoBase is Test, Constants, Events { vm.prank(SHORT_EXECUTOR); GHO_STEWARD_V2.setControlledFacilitator(controlledFacilitators, true); - REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); + BUIDL_USDC_REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); + GSM_CONVERTER = new GsmConverter( + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); } function ghoFaucet(address to, uint256 amount) public { diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index f44aad18..4a2fb5bd 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -13,15 +13,15 @@ contract TestGsmConverter is TestGhoBase { function testConstructor() public { GsmConverter gsmConverter = new GsmConverter( - address(GHO_GSM), - address(REDEMPTION), + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), address(BUIDL_TOKEN), address(USDC_TOKEN) ); - assertEq(gsmConverter.GSM(), address(GHO_GSM), 'Unexpected GSM address'); + assertEq(gsmConverter.GSM(), address(GHO_BUIDL_GSM), 'Unexpected GSM address'); assertEq( gsmConverter.REDEMPTION_CONTRACT(), - address(REDEMPTION), + address(BUIDL_USDC_REDEMPTION), 'Unexpected redemption contract address' ); assertEq( @@ -38,15 +38,49 @@ contract TestGsmConverter is TestGhoBase { function testRevertConstructorZeroAddressParams() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - new GsmConverter(address(0), address(REDEMPTION), address(BUIDL_TOKEN), address(USDC_TOKEN)); + new GsmConverter( + address(0), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - new GsmConverter(address(GHO_GSM), address(0), address(BUIDL_TOKEN), address(USDC_TOKEN)); + new GsmConverter(address(GHO_BUIDL_GSM), address(0), address(BUIDL_TOKEN), address(USDC_TOKEN)); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - new GsmConverter(address(GHO_GSM), address(REDEMPTION), address(0), address(USDC_TOKEN)); + new GsmConverter( + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), + address(0), + address(USDC_TOKEN) + ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - new GsmConverter(address(GHO_GSM), address(REDEMPTION), address(BUIDL_TOKEN), address(0)); + new GsmConverter( + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_TOKEN), + address(0) + ); + } + + function testRevertBuyAssetZeroAmount() public { + vm.expectRevert('INVALID_MIN_AMOUNT'); + uint256 invalidAmount = 0; + GSM_CONVERTER.buyAsset(invalidAmount, ALICE); + } + + function testBuyAsset() public { + // Supply assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // console2.log(BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM))); + // console2.log(BUIDL_TOKEN.balanceOf(ALICE)); } } diff --git a/src/test/helpers/Constants.sol b/src/test/helpers/Constants.sol index 68952b7b..98dd2770 100644 --- a/src/test/helpers/Constants.sol +++ b/src/test/helpers/Constants.sol @@ -46,7 +46,9 @@ contract Constants { uint256 constant DEFAULT_GSM_BUY_FEE = 0.1e4; // 10% uint256 constant DEFAULT_GSM_SELL_FEE = 0.1e4; // 10% uint128 constant DEFAULT_GSM_USDC_EXPOSURE = 100_000_000e6; // 6 decimals for USDC + uint128 constant DEFAULT_GSM_BUIDL_EXPOSURE = 100_000_000e6; // 6 decimals for BUIDL uint128 constant DEFAULT_GSM_USDC_AMOUNT = 100e6; // 6 decimals for USDC + uint128 constant DEFAULT_GSM_BUIDL_AMOUNT = 100e6; // 6 decimals for BUIDL uint128 constant DEFAULT_GSM_GHO_AMOUNT = 100e18; // GhoSteward From 57603796860201b6ec9ff5e348ea4a721d40bc19 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 10:50:43 -0500 Subject: [PATCH 09/68] test: add check on buyAsset for minAmount --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 5293deeb..0dcb2de9 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -54,6 +54,7 @@ contract GsmConverter is IGsmConverter { /// @inheritdoc IGsmConverter function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { + require(minAmount > 0, 'INVALID_MIN_AMOUNT'); IGhoToken(IGsm(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, receiver); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); @@ -68,4 +69,7 @@ contract GsmConverter is IGsmConverter { // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently // - send BUIDL to GSM, get GHO from GSM // - send GHO to user, safeTransfer + + // TODO: + // rescueTokens function to rescue any ERC20 tokens that are accidentally sent to this contract } From bb08c59b93588b7ea116230bb329263d865ae284 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 13:29:02 -0500 Subject: [PATCH 10/68] test: happy path for converter buyAsset --- .../gsm/converter/GsmConverter.sol | 12 ++++++++-- .../converter/interfaces/IGsmConverter.sol | 2 +- src/test/TestGhoBase.t.sol | 1 + src/test/TestGsmConverter.t.sol | 24 ++++++++++++++++++- src/test/mocks/MockRedemption.sol | 4 +++- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 0dcb2de9..65f4b1ee 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -55,9 +55,17 @@ contract GsmConverter is IGsmConverter { /// @inheritdoc IGsmConverter function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); - IGhoToken(IGsm(GSM).GHO_TOKEN()).transferFrom(msg.sender, address(this), minAmount); - (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, receiver); + + IGhoToken ghoToken = IGhoToken(IGsm(GSM).GHO_TOKEN()); + (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); + + ghoToken.transferFrom(msg.sender, address(this), ghoAmount); + ghoToken.approve(address(GSM), ghoAmount); + + (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); + IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); + // redeemableAssetAmount matches redeemedAssetAmount because Redemption exchanges in 1:1 ratio IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); emit BuyAssetThroughRedemption(msg.sender, receiver, redeemableAssetAmount, ghoSold); diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index e7bfcfb6..4700efaa 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -24,7 +24,7 @@ interface IGsmConverter { /** * @notice Buys the GSM underlying asset in exchange for selling GHO, after asset redemption - * @param minAmount The minimum amount of the underlying asset to buy + * @param minAmount The minimum amount of the underlying asset to buy (ie redeemed USDC) * @param receiver Recipient address of the underlying asset being purchased * @return The amount of underlying asset bought, after asset redemption * @return The amount of GHO sold by the user diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 738f6891..edd60a7d 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -303,6 +303,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); GHO_GSM_4626.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); + GHO_BUIDL_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); GHO_GSM.grantRole(GSM_LIQUIDATOR_ROLE, address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM.grantRole(GSM_SWAP_FREEZER_ROLE, address(GHO_GSM_SWAP_FREEZER)); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 4a2fb5bd..0f72d07b 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -72,7 +72,9 @@ contract TestGsmConverter is TestGhoBase { } function testBuyAsset() public { - // Supply assets to the BUIDL GSM first + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(ALICE); @@ -80,7 +82,27 @@ contract TestGsmConverter is TestGhoBase { GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + console2.log('test', DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Supply assets to another user + ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + (uint256 redeemedAssetAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + BOB + ); + + assertEq(redeemedAssetAmount, USDC_TOKEN.balanceOf(BOB), 'Unexpected redeemed buyAsset amount'); + + // console2.log(redeemableAssetAmount, ghoSold); + // console2.log(BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM))); // console2.log(BUIDL_TOKEN.balanceOf(ALICE)); + // console2.log(GHO_TOKEN.balanceOf(ALICE)); } } diff --git a/src/test/mocks/MockRedemption.sol b/src/test/mocks/MockRedemption.sol index ecb60ff3..369ae404 100644 --- a/src/test/mocks/MockRedemption.sol +++ b/src/test/mocks/MockRedemption.sol @@ -58,6 +58,8 @@ contract MockRedemption is IRedemption { * @inheritdoc IRedemption */ function redeem(uint256 amount) external { - // Intentionally left blank. + // mock outcome from Redemption + IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); + IERC20(liquidity).safeTransfer(msg.sender, amount); } } From eb4a8882e366bfd361fc0ee6be52a06e0adda58a Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 15:28:50 -0500 Subject: [PATCH 11/68] test: events testing + happy path buyAsset --- src/test/TestGsmConverter.t.sol | 41 ++++++++++++++++++++++++++------- src/test/helpers/Events.sol | 8 +++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 0f72d07b..29de1f24 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -72,7 +72,12 @@ contract TestGsmConverter is TestGhoBase { } function testBuyAsset() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + // USDC is redeemed for BUIDL in 1:1 ratio + uint256 expectedUSDCAmount = DEFAULT_GSM_BUIDL_AMOUNT; // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); @@ -86,23 +91,43 @@ contract TestGsmConverter is TestGhoBase { vm.prank(FAUCET); USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); - console2.log('test', DEFAULT_GSM_GHO_AMOUNT + buyFee); - // Supply assets to another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); (uint256 redeemedAssetAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( DEFAULT_GSM_BUIDL_AMOUNT, BOB ); + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); assertEq(redeemedAssetAmount, USDC_TOKEN.balanceOf(BOB), 'Unexpected redeemed buyAsset amount'); - - // console2.log(redeemableAssetAmount, ghoSold); - - // console2.log(BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM))); - // console2.log(BUIDL_TOKEN.balanceOf(ALICE)); - // console2.log(GHO_TOKEN.balanceOf(ALICE)); + assertEq(USDC_TOKEN.balanceOf(BOB), expectedUSDCAmount, 'Unexpected buyer final USDC balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final USDC balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final BUIDL balance' + ); } } diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index 23488186..c24431e5 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -114,6 +114,14 @@ interface Events { // GhoSteward event StewardExpirationUpdated(uint40 oldStewardExpiration, uint40 newStewardExpiration); + // GsmConverter events + event BuyAssetThroughRedemption( + address indexed originator, + address indexed receiver, + uint256 redeemableAssetAmount, + uint256 ghoAmount + ); + // FixedRateStrategyFactory event RateStrategyCreated(address indexed strategy, uint256 indexed rate); From 8ca6481ba8df3fe39d6adb558c8f66a9c7eb63e1 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 15:40:44 -0500 Subject: [PATCH 12/68] test: test revert cases --- src/test/TestGsmConverter.t.sol | 125 ++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 8 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 29de1f24..c5af138c 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -65,12 +65,6 @@ contract TestGsmConverter is TestGhoBase { ); } - function testRevertBuyAssetZeroAmount() public { - vm.expectRevert('INVALID_MIN_AMOUNT'); - uint256 invalidAmount = 0; - GSM_CONVERTER.buyAsset(invalidAmount, ALICE); - } - function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); @@ -97,13 +91,14 @@ contract TestGsmConverter is TestGhoBase { GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); - (uint256 redeemedAssetAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( DEFAULT_GSM_BUIDL_AMOUNT, BOB ); + vm.stopPrank(); assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - assertEq(redeemedAssetAmount, USDC_TOKEN.balanceOf(BOB), 'Unexpected redeemed buyAsset amount'); + assertEq(redeemedUSDCAmount, expectedUSDCAmount, 'Unexpected redeemed buyAsset amount'); assertEq(USDC_TOKEN.balanceOf(BOB), expectedUSDCAmount, 'Unexpected buyer final USDC balance'); assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( @@ -130,4 +125,118 @@ contract TestGsmConverter is TestGhoBase { 'Unexpected converter final BUIDL balance' ); } + + function testBuyAssetSendToOther() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + // USDC is redeemed for BUIDL in 1:1 ratio + uint256 expectedUSDCAmount = DEFAULT_GSM_BUIDL_AMOUNT; + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption(BOB, CHARLES, expectedRedeemedAssetAmount, expectedGhoSold); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + CHARLES + ); + vm.stopPrank(); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq(redeemedUSDCAmount, expectedUSDCAmount, 'Unexpected redeemed buyAsset amount'); + assertEq( + USDC_TOKEN.balanceOf(CHARLES), + expectedUSDCAmount, + 'Unexpected buyer final USDC balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final USDC balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final BUIDL balance' + ); + } + + function testRevertBuyAssetZeroAmount() public { + vm.expectRevert('INVALID_MIN_AMOUNT'); + uint256 invalidAmount = 0; + GSM_CONVERTER.buyAsset(invalidAmount, ALICE); + } + + function testRevertBuyAssetNoGHO() public { + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.expectRevert(stdError.arithmeticError); + GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, CHARLES); + vm.stopPrank(); + } + + function testRevertBuyAssetNoAllowance() public { + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + vm.startPrank(BOB); + vm.expectRevert(stdError.arithmeticError); + GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, CHARLES); + vm.stopPrank(); + } } From 2942b360aab100077087cebe610089b08a037608 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 16:18:06 -0500 Subject: [PATCH 13/68] fix: resolve tests involving facilitator due to new one added --- src/test/TestGhoBase.t.sol | 3 +-- src/test/TestGhoToken.t.sol | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index edd60a7d..26b84722 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -312,9 +312,8 @@ contract TestGhoBase is Test, Constants, Events { GHO_TOKEN.addFacilitator(address(GHO_GSM), 'GSM Facilitator', DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(address(GHO_GSM_4626), 'GSM 4626 Facilitator', DEFAULT_CAPACITY); - GHO_TOKEN.addFacilitator(address(GHO_BUIDL_GSM), 'GSM BUIDL Facilitator', DEFAULT_CAPACITY); - GHO_TOKEN.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); + GHO_TOKEN.addFacilitator(address(GHO_BUIDL_GSM), 'GSM BUIDL Facilitator', DEFAULT_CAPACITY); GHO_GSM_REGISTRY = new GsmRegistry(address(this)); GHO_STEWARD = new GhoSteward( diff --git a/src/test/TestGhoToken.t.sol b/src/test/TestGhoToken.t.sol index c3f94148..8f297769 100644 --- a/src/test/TestGhoToken.t.sol +++ b/src/test/TestGhoToken.t.sol @@ -43,7 +43,7 @@ contract TestGhoToken is TestGhoBase { function testGetPopulatedFacilitatorsList() public { address[] memory facilitatorList = GHO_TOKEN.getFacilitatorsList(); - assertEq(facilitatorList.length, 6, 'Unexpected number of facilitators'); + assertEq(facilitatorList.length, 7, 'Unexpected number of facilitators'); assertEq(facilitatorList[0], address(GHO_ATOKEN), 'Unexpected address for mock facilitator 1'); assertEq( facilitatorList[1], From ebca6d72c15c0d585b8c2e6d5ba8580be8b53ec2 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 17:54:16 -0500 Subject: [PATCH 14/68] fix: resolve fuzz test input with zero address --- src/test/TestGsmRegistry.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/TestGsmRegistry.t.sol b/src/test/TestGsmRegistry.t.sol index 4b135e1b..0c67bd59 100644 --- a/src/test/TestGsmRegistry.t.sol +++ b/src/test/TestGsmRegistry.t.sol @@ -6,6 +6,7 @@ import './TestGhoBase.t.sol'; contract TestGsmRegistry is TestGhoBase { function testConstructor(address newOwner) public { vm.assume(newOwner != address(this)); + vm.assume(newOwner != address(0)); vm.expectEmit(true, true, false, true); emit OwnershipTransferred(address(0), address(this)); From cff5b1fe2f08c272b32efe13386926233ef39b84 Mon Sep 17 00:00:00 2001 From: YBM Date: Thu, 5 Sep 2024 18:01:33 -0500 Subject: [PATCH 15/68] feat: rescueTokens implementation + tests --- .../gsm/converter/GsmConverter.sol | 32 ++++-- .../converter/interfaces/IGsmConverter.sol | 34 ++++++- src/test/TestGhoBase.t.sol | 1 + src/test/TestGsmConverter.t.sol | 98 ++++++++++++++++++- 4 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 65f4b1ee..22cc5eb7 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.10; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; @@ -13,9 +14,15 @@ import {IRedemption} from '../dependencies/circle/IRedemption.sol'; * @author Aave * @notice Converter that facilitates conversions/redemptions of underlying assets. Integrates with GSM to buy/sell to go to/from an underlying asset to/from GHO. */ -contract GsmConverter is IGsmConverter { +contract GsmConverter is AccessControl, IGsmConverter { using GPv2SafeERC20 for IERC20; + /// @inheritdoc IGsmConverter + bytes32 public immutable TOKEN_RESCUER_ROLE; + + /// @inheritdoc IGsmConverter + address public immutable GHO_TOKEN; + /// @inheritdoc IGsmConverter address public immutable GSM; @@ -36,11 +43,13 @@ contract GsmConverter is IGsmConverter { * @param redeemedAsset The address of the asset being received from redemption */ constructor( + address admin, address gsm, address redemptionContract, address redeemableAsset, address redeemedAsset ) { + require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); @@ -50,17 +59,20 @@ contract GsmConverter is IGsmConverter { REDEMPTION_CONTRACT = redemptionContract; REDEEMABLE_ASSET = redeemableAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC + TOKEN_RESCUER_ROLE = IGsm(GSM).TOKEN_RESCUER_ROLE(); + GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); } /// @inheritdoc IGsmConverter function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); - IGhoToken ghoToken = IGhoToken(IGsm(GSM).GHO_TOKEN()); (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); - ghoToken.transferFrom(msg.sender, address(this), ghoAmount); - ghoToken.approve(address(GSM), ghoAmount); + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoAmount); + IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); @@ -78,6 +90,14 @@ contract GsmConverter is IGsmConverter { // - send BUIDL to GSM, get GHO from GSM // - send GHO to user, safeTransfer - // TODO: - // rescueTokens function to rescue any ERC20 tokens that are accidentally sent to this contract + /// @inheritdoc IGsmConverter + function rescueTokens( + address token, + address to, + uint256 amount + ) external onlyRole(TOKEN_RESCUER_ROLE) { + require(amount > 0, 'INVALID_AMOUNT'); + IERC20(token).safeTransfer(to, amount); + emit TokensRescued(token, to, amount); + } } diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 4700efaa..3d7e6c1c 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -22,15 +22,35 @@ interface IGsmConverter { uint256 ghoAmount ); + /** + * @dev Emitted when tokens are rescued from the GSM converter + * @param tokenRescued The address of the rescued token + * @param recipient The address that received the rescued tokens + * @param amountRescued The amount of token rescued + */ + event TokensRescued( + address indexed tokenRescued, + address indexed recipient, + uint256 amountRescued + ); + /** * @notice Buys the GSM underlying asset in exchange for selling GHO, after asset redemption - * @param minAmount The minimum amount of the underlying asset to buy (ie redeemed USDC) + * @param minAmount The minimum amount of the underlying asset to buy (ie BUIDL) * @param receiver Recipient address of the underlying asset being purchased * @return The amount of underlying asset bought, after asset redemption * @return The amount of GHO sold by the user */ function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256); + /** + * @notice Rescue and transfer tokens locked in this contract + * @param token The address of the token + * @param to The address of the recipient + * @param amount The amount of token to transfer + */ + function rescueTokens(address token, address to, uint256 amount) external; + /** * @notice Sells the GSM underlying asset in exchange for buying GHO, after asset conversion * @param maxAmount The maximum amount of the underlying asset to sell @@ -40,6 +60,12 @@ interface IGsmConverter { */ // function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); + /** + * @notice Returns the address of the GHO token + * @return The address of GHO token contract + */ + function GHO_TOKEN() external view returns (address); + /** * @notice Returns the address of the GSM contract associated with the converter * @return The address of the GSM contract @@ -63,4 +89,10 @@ interface IGsmConverter { * @return The address of the redemption contract */ function REDEMPTION_CONTRACT() external view returns (address); + + /** + * @notice Returns the identifier of the Token Rescuer Role + * @return The bytes32 id hash of the TokenRescuer role + */ + function TOKEN_RESCUER_ROLE() external view returns (bytes32); } diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 26b84722..905b64dd 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -341,6 +341,7 @@ contract TestGhoBase is Test, Constants, Events { BUIDL_USDC_REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); GSM_CONVERTER = new GsmConverter( + address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), address(BUIDL_TOKEN), diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index c5af138c..8bdf26e4 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -13,11 +13,16 @@ contract TestGsmConverter is TestGhoBase { function testConstructor() public { GsmConverter gsmConverter = new GsmConverter( + address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), address(BUIDL_TOKEN), address(USDC_TOKEN) ); + assertTrue( + gsmConverter.hasRole(gsmConverter.DEFAULT_ADMIN_ROLE(), address(this)), + 'Unexpected default admin address' + ); assertEq(gsmConverter.GSM(), address(GHO_BUIDL_GSM), 'Unexpected GSM address'); assertEq( gsmConverter.REDEMPTION_CONTRACT(), @@ -40,16 +45,33 @@ contract TestGsmConverter is TestGhoBase { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new GsmConverter( address(0), + address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), address(BUIDL_TOKEN), address(USDC_TOKEN) ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - new GsmConverter(address(GHO_BUIDL_GSM), address(0), address(BUIDL_TOKEN), address(USDC_TOKEN)); + new GsmConverter( + address(this), + address(0), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(0), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter( + address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), address(0), @@ -58,6 +80,7 @@ contract TestGsmConverter is TestGhoBase { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new GsmConverter( + address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), address(BUIDL_TOKEN), @@ -239,4 +262,77 @@ contract TestGsmConverter is TestGhoBase { GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, CHARLES); vm.stopPrank(); } + + function testRescueTokens() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + + vm.prank(FAUCET); + WETH.mint(address(GSM_CONVERTER), 100e18); + assertEq(WETH.balanceOf(address(GSM_CONVERTER)), 100e18, 'Unexpected GSM WETH before balance'); + assertEq(WETH.balanceOf(ALICE), 0, 'Unexpected target WETH before balance'); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit TokensRescued(address(WETH), ALICE, 100e18); + GSM_CONVERTER.rescueTokens(address(WETH), ALICE, 100e18); + assertEq(WETH.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM WETH after balance'); + assertEq(WETH.balanceOf(ALICE), 100e18, 'Unexpected target WETH after balance'); + } + + function testRevertRescueTokensZeroAmount() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + vm.expectRevert('INVALID_AMOUNT'); + GSM_CONVERTER.rescueTokens(address(WETH), ALICE, 0); + } + + function testRevertRescueTokensInsufficientAmount() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + vm.expectRevert(); + GSM_CONVERTER.rescueTokens(address(WETH), ALICE, 1); + } + + function testRescueGhoTokens() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + + ghoFaucet(address(GSM_CONVERTER), 100e18); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 100e18, + 'Unexpected GSM GHO before balance' + ); + assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected target GHO before balance'); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit TokensRescued(address(GHO_TOKEN), ALICE, 100e18); + GSM_CONVERTER.rescueTokens(address(GHO_TOKEN), ALICE, 100e18); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM GHO after balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), 100e18, 'Unexpected target GHO after balance'); + } + + function testRescueRedeemedTokens() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + + vm.prank(FAUCET); + USDC_TOKEN.mint(address(GSM_CONVERTER), DEFAULT_GSM_USDC_AMOUNT); + + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected USDC balance before'); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit TokensRescued(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); + GSM_CONVERTER.rescueTokens(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); + assertEq(USDC_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC balance after'); + } + + function testRescueRedeemableTokens() public { + GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); + + vm.prank(FAUCET); + BUIDL_TOKEN.mint(address(GSM_CONVERTER), DEFAULT_GSM_USDC_AMOUNT); + + assertEq(BUIDL_TOKEN.balanceOf(ALICE), 0, 'Unexpected BUIDL balance before'); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit TokensRescued(address(BUIDL_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); + GSM_CONVERTER.rescueTokens(address(BUIDL_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + DEFAULT_GSM_USDC_AMOUNT, + 'Unexpected BUIDL balance after' + ); + } } From 4d2abe2dc542459539596fa3ef49633633051b0d Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 09:59:27 -0500 Subject: [PATCH 16/68] fix: convert to Ownable --- .../facilitators/gsm/converter/GsmConverter.sol | 16 ++++------------ .../gsm/converter/interfaces/IGsmConverter.sol | 6 ------ src/test/TestGsmConverter.t.sol | 15 +-------------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 22cc5eb7..cacc89ae 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.10; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; @@ -14,12 +14,9 @@ import {IRedemption} from '../dependencies/circle/IRedemption.sol'; * @author Aave * @notice Converter that facilitates conversions/redemptions of underlying assets. Integrates with GSM to buy/sell to go to/from an underlying asset to/from GHO. */ -contract GsmConverter is AccessControl, IGsmConverter { +contract GsmConverter is Ownable, IGsmConverter { using GPv2SafeERC20 for IERC20; - /// @inheritdoc IGsmConverter - bytes32 public immutable TOKEN_RESCUER_ROLE; - /// @inheritdoc IGsmConverter address public immutable GHO_TOKEN; @@ -59,10 +56,9 @@ contract GsmConverter is AccessControl, IGsmConverter { REDEMPTION_CONTRACT = redemptionContract; REDEEMABLE_ASSET = redeemableAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC - TOKEN_RESCUER_ROLE = IGsm(GSM).TOKEN_RESCUER_ROLE(); GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); - _grantRole(DEFAULT_ADMIN_ROLE, admin); + transferOwnership(admin); } /// @inheritdoc IGsmConverter @@ -91,11 +87,7 @@ contract GsmConverter is AccessControl, IGsmConverter { // - send GHO to user, safeTransfer /// @inheritdoc IGsmConverter - function rescueTokens( - address token, - address to, - uint256 amount - ) external onlyRole(TOKEN_RESCUER_ROLE) { + function rescueTokens(address token, address to, uint256 amount) external onlyOwner { require(amount > 0, 'INVALID_AMOUNT'); IERC20(token).safeTransfer(to, amount); emit TokensRescued(token, to, amount); diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 3d7e6c1c..0da148f7 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -89,10 +89,4 @@ interface IGsmConverter { * @return The address of the redemption contract */ function REDEMPTION_CONTRACT() external view returns (address); - - /** - * @notice Returns the identifier of the Token Rescuer Role - * @return The bytes32 id hash of the TokenRescuer role - */ - function TOKEN_RESCUER_ROLE() external view returns (bytes32); } diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 8bdf26e4..036d34fd 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -19,10 +19,7 @@ contract TestGsmConverter is TestGhoBase { address(BUIDL_TOKEN), address(USDC_TOKEN) ); - assertTrue( - gsmConverter.hasRole(gsmConverter.DEFAULT_ADMIN_ROLE(), address(this)), - 'Unexpected default admin address' - ); + assertEq(gsmConverter.owner(), address(this), 'Unexpected default admin address'); assertEq(gsmConverter.GSM(), address(GHO_BUIDL_GSM), 'Unexpected GSM address'); assertEq( gsmConverter.REDEMPTION_CONTRACT(), @@ -264,8 +261,6 @@ contract TestGsmConverter is TestGhoBase { } function testRescueTokens() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); - vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); assertEq(WETH.balanceOf(address(GSM_CONVERTER)), 100e18, 'Unexpected GSM WETH before balance'); @@ -278,20 +273,16 @@ contract TestGsmConverter is TestGhoBase { } function testRevertRescueTokensZeroAmount() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INVALID_AMOUNT'); GSM_CONVERTER.rescueTokens(address(WETH), ALICE, 0); } function testRevertRescueTokensInsufficientAmount() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert(); GSM_CONVERTER.rescueTokens(address(WETH), ALICE, 1); } function testRescueGhoTokens() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); - ghoFaucet(address(GSM_CONVERTER), 100e18); assertEq( GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), @@ -307,8 +298,6 @@ contract TestGsmConverter is TestGhoBase { } function testRescueRedeemedTokens() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); - vm.prank(FAUCET); USDC_TOKEN.mint(address(GSM_CONVERTER), DEFAULT_GSM_USDC_AMOUNT); @@ -320,8 +309,6 @@ contract TestGsmConverter is TestGhoBase { } function testRescueRedeemableTokens() public { - GSM_CONVERTER.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); - vm.prank(FAUCET); BUIDL_TOKEN.mint(address(GSM_CONVERTER), DEFAULT_GSM_USDC_AMOUNT); From 2faf8a66f89ff19ef7545b665c153d22b3ac5aae Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 11:16:45 -0500 Subject: [PATCH 17/68] fix: validate calculated ghoAmount matches ghoSold, reset approvals, enforce no remaining balance in converter --- .../facilitators/gsm/converter/GsmConverter.sol | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index cacc89ae..7ec3c6d4 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -66,16 +66,25 @@ contract GsmConverter is Ownable, IGsmConverter { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); - IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); + require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); + IGhoToken(GHO_TOKEN).approve(address(GSM), 0); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); + IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), 0); // redeemableAssetAmount matches redeemedAssetAmount because Redemption exchanges in 1:1 ratio IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); + require(IGhoToken(GHO_TOKEN).balanceOf(address(this)) == 0, 'INVALID_REMAINING_GHO_BALANCE'); + require( + IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == 0, + 'INVALID_REMAINING_GHO_BALANCE' + ); + require(IERC20(REDEEMED_ASSET).balanceOf(address(this)) == 0, 'INVALID_REMAINING_GHO_BALANCE'); + emit BuyAssetThroughRedemption(msg.sender, receiver, redeemableAssetAmount, ghoSold); return (redeemableAssetAmount, ghoSold); } From bc05a7dabffa825989516b973677763ec659431e Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 14:52:36 -0500 Subject: [PATCH 18/68] test: create mockGSM to test unmatching gho amount case --- .../gsm/converter/GsmConverter.sol | 17 +- src/test/TestGhoBase.t.sol | 1 + src/test/TestGsmConverter.t.sol | 47 ++ src/test/mocks/MockGsmFailed.sol | 571 ++++++++++++++++++ 4 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 src/test/mocks/MockGsmFailed.sol diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 7ec3c6d4..1036b5c2 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -65,7 +65,12 @@ contract GsmConverter is Ownable, IGsmConverter { function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); + uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); + uint256 initialRedeemableAssetBalance = IERC20(REDEEMABLE_ASSET).balanceOf(address(this)); + uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); + (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); @@ -78,12 +83,18 @@ contract GsmConverter is Ownable, IGsmConverter { // redeemableAssetAmount matches redeemedAssetAmount because Redemption exchanges in 1:1 ratio IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); - require(IGhoToken(GHO_TOKEN).balanceOf(address(this)) == 0, 'INVALID_REMAINING_GHO_BALANCE'); require( - IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == 0, + IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, 'INVALID_REMAINING_GHO_BALANCE' ); - require(IERC20(REDEEMED_ASSET).balanceOf(address(this)) == 0, 'INVALID_REMAINING_GHO_BALANCE'); + require( + IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == initialRedeemableAssetBalance, + 'INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE' + ); + require( + IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance, + 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' + ); emit BuyAssetThroughRedemption(msg.sender, receiver, redeemableAssetAmount, ghoSold); return (redeemableAssetAmount, ghoSold); diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 905b64dd..a890f099 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -22,6 +22,7 @@ import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; +import {MockGsmFailed} from './mocks/MockGsmFailed.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockAddressesProvider} from './mocks/MockAddressesProvider.sol'; import {MockERC4626} from './mocks/MockERC4626.sol'; diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 036d34fd..4036bb1a 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -322,4 +322,51 @@ contract TestGsmConverter is TestGhoBase { 'Unexpected BUIDL balance after' ); } + + function _upgradeToFailedGSM() internal { + address gsmFailed = address( + new MockGsmFailed( + address(GHO_TOKEN), + address(BUIDL_TOKEN), + address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) + ) + ); + bytes memory data = abi.encodeWithSelector( + MockGsmFailed.initialize.selector, + address(this), + TREASURY, + DEFAULT_GSM_USDC_EXPOSURE + ); + + vm.prank(SHORT_EXECUTOR); + AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); + } + + function testRevertBuyAssetInvalidGhoSold() public { + _upgradeToFailedGSM(); + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + vm.expectRevert('INVALID_GHO_SOLD'); + GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + vm.stopPrank(); + } } diff --git a/src/test/mocks/MockGsmFailed.sol b/src/test/mocks/MockGsmFailed.sol new file mode 100644 index 00000000..1d583371 --- /dev/null +++ b/src/test/mocks/MockGsmFailed.sol @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; +import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {IGhoFacilitator} from '../../contracts/gho/interfaces/IGhoFacilitator.sol'; +import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; +import {IGsmPriceStrategy} from '../../contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; +import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; + +/** + * @title MockGsmFailed + * @author Aave + * @notice GSM that fails calculation for GHO amount in getGhoAmountForBuyAsset + */ +contract MockGsmFailed is AccessControl, VersionedInitializable, EIP712, IGsm { + using GPv2SafeERC20 for IERC20; + using SafeCast for uint256; + + /// @inheritdoc IGsm + bytes32 public constant CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + address public immutable GHO_TOKEN; + + /// @inheritdoc IGsm + address public immutable UNDERLYING_ASSET; + + /// @inheritdoc IGsm + address public immutable PRICE_STRATEGY; + + /// @inheritdoc IGsm + mapping(address => uint256) public nonces; + + address internal _ghoTreasury; + address internal _feeStrategy; + bool internal _isFrozen; + bool internal _isSeized; + uint128 internal _exposureCap; + uint128 internal _currentExposure; + uint128 internal _accruedFees; + + /** + * @dev Require GSM to not be frozen for functions marked by this modifier + */ + modifier notFrozen() { + require(!_isFrozen, 'GSM_FROZEN'); + _; + } + + /** + * @dev Require GSM to not be seized for functions marked by this modifier + */ + modifier notSeized() { + require(!_isSeized, 'GSM_SEIZED'); + _; + } + + /** + * @dev Constructor + * @param ghoToken The address of the GHO token contract + * @param underlyingAsset The address of the collateral asset + * @param priceStrategy The address of the price strategy + */ + constructor(address ghoToken, address underlyingAsset, address priceStrategy) EIP712('GSM', '1') { + require(ghoToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require( + IGsmPriceStrategy(priceStrategy).UNDERLYING_ASSET() == underlyingAsset, + 'INVALID_PRICE_STRATEGY' + ); + GHO_TOKEN = ghoToken; + UNDERLYING_ASSET = underlyingAsset; + PRICE_STRATEGY = priceStrategy; + } + + /** + * @notice GSM initializer + * @param admin The address of the default admin role + * @param ghoTreasury The address of the GHO treasury + * @param exposureCap Maximum amount of user-supplied underlying asset in GSM + */ + function initialize( + address admin, + address ghoTreasury, + uint128 exposureCap + ) external initializer { + require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(CONFIGURATOR_ROLE, admin); + _updateGhoTreasury(ghoTreasury); + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGsm + function buyAsset( + uint256 minAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _buyAsset(msg.sender, minAmount, receiver); + } + + /// @inheritdoc IGsm + function buyAssetWithSig( + address originator, + uint256 minAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _buyAsset(originator, minAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAsset( + uint256 maxAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _sellAsset(msg.sender, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAssetWithSig( + address originator, + uint256 maxAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _sellAsset(originator, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function rescueTokens( + address token, + address to, + uint256 amount + ) external onlyRole(TOKEN_RESCUER_ROLE) { + require(amount > 0, 'INVALID_AMOUNT'); + if (token == GHO_TOKEN) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; + require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); + } + if (token == UNDERLYING_ASSET) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; + require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); + } + IERC20(token).safeTransfer(to, amount); + emit TokensRescued(token, to, amount); + } + + /// @inheritdoc IGsm + function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { + if (enable) { + require(!_isFrozen, 'GSM_ALREADY_FROZEN'); + } else { + require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); + } + _isFrozen = enable; + emit SwapFreeze(msg.sender, enable); + } + + /// @inheritdoc IGsm + function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + _isSeized = true; + _currentExposure = 0; + _updateExposureCap(0); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); + if (underlyingBalance > 0) { + IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); + } + + emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); + return underlyingBalance; + } + + /// @inheritdoc IGsm + function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + require(_isSeized, 'GSM_NOT_SEIZED'); + require(amount > 0, 'INVALID_AMOUNT'); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + if (amount > ghoMinted) { + amount = ghoMinted; + } + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); + IGhoToken(GHO_TOKEN).burn(amount); + + emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); + return amount; + } + + /// @inheritdoc IGsm + function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { + _updateFeeStrategy(feeStrategy); + } + + /// @inheritdoc IGsm + function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGhoFacilitator + function distributeFeesToTreasury() public virtual override { + uint256 accruedFees = _accruedFees; + if (accruedFees > 0) { + _accruedFees = 0; + IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); + emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); + } + } + + /// @inheritdoc IGhoFacilitator + function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { + _updateGhoTreasury(newGhoTreasury); + } + + /// @inheritdoc IGsm + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IGsm + function getGhoAmountForBuyAsset( + uint256 minAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAssetAmount); + // invalid amount of ghoAmount returned + return (assetAmount, ghoSold * 2, grossAmount, fee); + } + + /// @inheritdoc IGsm + function getGhoAmountForSellAsset( + uint256 maxAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + return _calculateGhoAmountForSellAsset(maxAssetAmount); + } + + /// @inheritdoc IGsm + function getAssetAmountForBuyAsset( + uint256 maxGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) + : maxGhoAmount; + // round down so maxGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + true + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAssetAmountForSellAsset( + uint256 minGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(minGhoAmount) + : minGhoAmount; + // round up so minGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, true); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + false + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAvailableUnderlyingExposure() external view returns (uint256) { + return _exposureCap > _currentExposure ? _exposureCap - _currentExposure : 0; + } + + /// @inheritdoc IGsm + function getExposureCap() external view returns (uint128) { + return _exposureCap; + } + + /// @inheritdoc IGsm + function getAvailableLiquidity() external view returns (uint256) { + return _currentExposure; + } + + /// @inheritdoc IGsm + function getFeeStrategy() external view returns (address) { + return _feeStrategy; + } + + /// @inheritdoc IGsm + function getAccruedFees() external view returns (uint256) { + return _accruedFees; + } + + /// @inheritdoc IGsm + function getIsFrozen() external view returns (bool) { + return _isFrozen; + } + + /// @inheritdoc IGsm + function getIsSeized() external view returns (bool) { + return _isSeized; + } + + /// @inheritdoc IGsm + function canSwap() external view returns (bool) { + return !_isFrozen && !_isSeized; + } + + /// @inheritdoc IGhoFacilitator + function getGhoTreasury() external view override returns (address) { + return _ghoTreasury; + } + + /// @inheritdoc IGsm + function GSM_REVISION() public pure virtual override returns (uint256) { + return 2; + } + + /** + * @dev Buys an underlying asset with GHO + * @param originator The originator of the request + * @param minAmount The minimum amount of the underlying asset desired for purchase + * @param receiver The recipient address of the underlying asset being purchased + * @return The amount of underlying asset bought + * @return The amount of GHO sold by the user + */ + function _buyAsset( + address originator, + uint256 minAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAmount); + + _beforeBuyAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); + + _currentExposure -= uint128(assetAmount); + _accruedFees += fee.toUint128(); + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); + IGhoToken(GHO_TOKEN).burn(grossAmount); + IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); + + emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); + return (assetAmount, ghoSold); + } + + /** + * @dev Hook that is called before `buyAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired for purchase + * @param receiver Recipient address of the underlying asset being purchased + */ + function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} + + /** + * @dev Sells an underlying asset for GHO + * @param originator The originator of the request + * @param maxAmount The maximum amount of the underlying asset desired to sell + * @param receiver The recipient address of the GHO being purchased + * @return The amount of underlying asset sold + * @return The amount of GHO bought by the user + */ + function _sellAsset( + address originator, + uint256 maxAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoBought, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForSellAsset(maxAmount); + + _beforeSellAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure + assetAmount <= _exposureCap, 'EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); + + _currentExposure += uint128(assetAmount); + _accruedFees += fee.toUint128(); + IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); + + IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); + IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); + + emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); + return (assetAmount, ghoBought); + } + + /** + * @dev Hook that is called before `sellAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired to sell + * @param receiver Recipient address of the GHO being purchased + */ + function _beforeSellAsset( + address originator, + uint256 amount, + address receiver + ) internal virtual {} + + /** + * @dev Returns the amount of GHO sold in exchange of buying underlying asset + * @param assetAmount The amount of underlying asset to buy + * @return The exact amount of asset the user purchases + * @return The total amount of GHO the user sells (gross amount in GHO plus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied on top of gross amount of GHO + */ + function _calculateGhoAmountForBuyAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the highest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; + uint256 ghoSold = grossAmount + fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) + : ghoSold; + // pick the lowest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + false + ); + uint256 finalFee = ghoSold - finalGrossAmount; + return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset + * @param assetAmount The amount of underlying asset to sell + * @return The exact amount of asset the user sells + * @return The total amount of GHO the user buys (gross amount in GHO minus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied to the gross amount of GHO + */ + function _calculateGhoAmountForSellAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the lowest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, false); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(grossAmount) : 0; + uint256 ghoBought = grossAmount - fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(ghoBought) + : ghoBought; + // pick the highest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + true + ); + uint256 finalFee = finalGrossAmount - ghoBought; + return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Updates Fee Strategy + * @param feeStrategy The address of the new Fee Strategy + */ + function _updateFeeStrategy(address feeStrategy) internal { + address oldFeeStrategy = _feeStrategy; + _feeStrategy = feeStrategy; + emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); + } + + /** + * @dev Updates Exposure Cap + * @param exposureCap The value of the new Exposure Cap + */ + function _updateExposureCap(uint128 exposureCap) internal { + uint128 oldExposureCap = _exposureCap; + _exposureCap = exposureCap; + emit ExposureCapUpdated(oldExposureCap, exposureCap); + } + + /** + * @dev Updates GHO Treasury Address + * @param newGhoTreasury The address of the new GHO Treasury + */ + function _updateGhoTreasury(address newGhoTreasury) internal { + require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); + address oldGhoTreasury = _ghoTreasury; + _ghoTreasury = newGhoTreasury; + emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return GSM_REVISION(); + } +} From 0c1beb29aa48cdfe001550d500bcb691260d1a43 Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 14:53:45 -0500 Subject: [PATCH 19/68] refactor: methods re-order --- src/test/TestGsmConverter.t.sol | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 4036bb1a..03fd55e5 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -260,6 +260,34 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } + function testRevertBuyAssetInvalidGhoSold() public { + _upgradeToFailedGSM(); + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + vm.expectRevert('INVALID_GHO_SOLD'); + GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + vm.stopPrank(); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); @@ -341,32 +369,4 @@ contract TestGsmConverter is TestGhoBase { vm.prank(SHORT_EXECUTOR); AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); } - - function testRevertBuyAssetInvalidGhoSold() public { - _upgradeToFailedGSM(); - - uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); - - // Supply BUIDL assets to the BUIDL GSM first - vm.prank(FAUCET); - BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); - vm.startPrank(ALICE); - BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); - GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); - vm.stopPrank(); - - // Supply USDC to the Redemption contract - vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); - - // Supply assets to another user - ghoFaucet(BOB, expectedGhoSold + buyFee); - vm.startPrank(BOB); - GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); - vm.expectRevert('INVALID_GHO_SOLD'); - GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); - vm.stopPrank(); - } } From 117eda82414702475ba735e98abfe4c4de0acb29 Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 15:22:21 -0500 Subject: [PATCH 20/68] refactor: rename mock test: remaining gho balance --- .../gsm/converter/GsmConverter.sol | 1 + src/test/TestGhoBase.t.sol | 3 +- src/test/TestGsmConverter.t.sol | 55 +- ...mFailed.sol => MockGsmFailedGhoAmount.sol} | 9 +- .../MockGsmFailedRemainingGhoBalance.sol | 571 ++++++++++++++++++ 5 files changed, 630 insertions(+), 9 deletions(-) rename src/test/mocks/{MockGsmFailed.sol => MockGsmFailedGhoAmount.sol} (98%) create mode 100644 src/test/mocks/MockGsmFailedRemainingGhoBalance.sol diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 1036b5c2..641dc1f9 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -76,6 +76,7 @@ contract GsmConverter is Ownable, IGsmConverter { (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); + IGhoToken(GHO_TOKEN).approve(address(GSM), 0); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index a890f099..84a71452 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -22,7 +22,8 @@ import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; -import {MockGsmFailed} from './mocks/MockGsmFailed.sol'; +import {MockGsmFailedGhoAmount} from './mocks/MockGsmFailedGhoAmount.sol'; +import {MockGsmFailedRemainingGhoBalance} from './mocks/MockGsmFailedRemainingGhoBalance.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockAddressesProvider} from './mocks/MockAddressesProvider.sol'; import {MockERC4626} from './mocks/MockERC4626.sol'; diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 03fd55e5..d2f2f43d 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -261,7 +261,7 @@ contract TestGsmConverter is TestGhoBase { } function testRevertBuyAssetInvalidGhoSold() public { - _upgradeToFailedGSM(); + _upgradeToGsmFailedGhoAmount(); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM @@ -288,6 +288,34 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } + function testRevertBuyAssetInvalidRemainingGhoBalance() public { + _upgradeToGsmFailedRemainingGhoBalance(); + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + vm.expectRevert('INVALID_REMAINING_GHO_BALANCE'); + GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + vm.stopPrank(); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); @@ -351,16 +379,35 @@ contract TestGsmConverter is TestGhoBase { ); } - function _upgradeToFailedGSM() internal { + function _upgradeToGsmFailedGhoAmount() internal { + address gsmFailed = address( + new MockGsmFailedGhoAmount( + address(GHO_TOKEN), + address(BUIDL_TOKEN), + address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) + ) + ); + bytes memory data = abi.encodeWithSelector( + MockGsmFailedGhoAmount.initialize.selector, + address(this), + TREASURY, + DEFAULT_GSM_USDC_EXPOSURE + ); + + vm.prank(SHORT_EXECUTOR); + AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); + } + + function _upgradeToGsmFailedRemainingGhoBalance() internal { address gsmFailed = address( - new MockGsmFailed( + new MockGsmFailedRemainingGhoBalance( address(GHO_TOKEN), address(BUIDL_TOKEN), address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) ) ); bytes memory data = abi.encodeWithSelector( - MockGsmFailed.initialize.selector, + MockGsmFailedRemainingGhoBalance.initialize.selector, address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE diff --git a/src/test/mocks/MockGsmFailed.sol b/src/test/mocks/MockGsmFailedGhoAmount.sol similarity index 98% rename from src/test/mocks/MockGsmFailed.sol rename to src/test/mocks/MockGsmFailedGhoAmount.sol index 1d583371..7d922666 100644 --- a/src/test/mocks/MockGsmFailed.sol +++ b/src/test/mocks/MockGsmFailedGhoAmount.sol @@ -15,11 +15,11 @@ import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/inte import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; /** - * @title MockGsmFailed + * @title MockGsmFailedGhoAmount * @author Aave * @notice GSM that fails calculation for GHO amount in getGhoAmountForBuyAsset */ -contract MockGsmFailed is AccessControl, VersionedInitializable, EIP712, IGsm { +contract MockGsmFailedGhoAmount is AccessControl, VersionedInitializable, EIP712, IGsm { using GPv2SafeERC20 for IERC20; using SafeCast for uint256; @@ -287,7 +287,7 @@ contract MockGsmFailed is AccessControl, VersionedInitializable, EIP712, IGsm { uint256 grossAmount, uint256 fee ) = _calculateGhoAmountForBuyAsset(minAssetAmount); - // invalid amount of ghoAmount returned + // TRIGGER ERROR: invalid amount of ghoSold value returned return (assetAmount, ghoSold * 2, grossAmount, fee); } @@ -411,7 +411,8 @@ contract MockGsmFailed is AccessControl, VersionedInitializable, EIP712, IGsm { _currentExposure -= uint128(assetAmount); _accruedFees += fee.toUint128(); - IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); + // TRIGGER ERROR: transfer incorrect GHO amount to GSM + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold - 1); IGhoToken(GHO_TOKEN).burn(grossAmount); IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); diff --git a/src/test/mocks/MockGsmFailedRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedRemainingGhoBalance.sol new file mode 100644 index 00000000..64eaa185 --- /dev/null +++ b/src/test/mocks/MockGsmFailedRemainingGhoBalance.sol @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; +import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {IGhoFacilitator} from '../../contracts/gho/interfaces/IGhoFacilitator.sol'; +import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; +import {IGsmPriceStrategy} from '../../contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; +import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; + +/** + * @title MockGsmFailedRemainingGhoBalance + * @author Aave + * @notice GSM that transfers incorrect amount of GHO during buyAsset + */ +contract MockGsmFailedRemainingGhoBalance is AccessControl, VersionedInitializable, EIP712, IGsm { + using GPv2SafeERC20 for IERC20; + using SafeCast for uint256; + + /// @inheritdoc IGsm + bytes32 public constant CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + address public immutable GHO_TOKEN; + + /// @inheritdoc IGsm + address public immutable UNDERLYING_ASSET; + + /// @inheritdoc IGsm + address public immutable PRICE_STRATEGY; + + /// @inheritdoc IGsm + mapping(address => uint256) public nonces; + + address internal _ghoTreasury; + address internal _feeStrategy; + bool internal _isFrozen; + bool internal _isSeized; + uint128 internal _exposureCap; + uint128 internal _currentExposure; + uint128 internal _accruedFees; + + /** + * @dev Require GSM to not be frozen for functions marked by this modifier + */ + modifier notFrozen() { + require(!_isFrozen, 'GSM_FROZEN'); + _; + } + + /** + * @dev Require GSM to not be seized for functions marked by this modifier + */ + modifier notSeized() { + require(!_isSeized, 'GSM_SEIZED'); + _; + } + + /** + * @dev Constructor + * @param ghoToken The address of the GHO token contract + * @param underlyingAsset The address of the collateral asset + * @param priceStrategy The address of the price strategy + */ + constructor(address ghoToken, address underlyingAsset, address priceStrategy) EIP712('GSM', '1') { + require(ghoToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require( + IGsmPriceStrategy(priceStrategy).UNDERLYING_ASSET() == underlyingAsset, + 'INVALID_PRICE_STRATEGY' + ); + GHO_TOKEN = ghoToken; + UNDERLYING_ASSET = underlyingAsset; + PRICE_STRATEGY = priceStrategy; + } + + /** + * @notice GSM initializer + * @param admin The address of the default admin role + * @param ghoTreasury The address of the GHO treasury + * @param exposureCap Maximum amount of user-supplied underlying asset in GSM + */ + function initialize( + address admin, + address ghoTreasury, + uint128 exposureCap + ) external initializer { + require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(CONFIGURATOR_ROLE, admin); + _updateGhoTreasury(ghoTreasury); + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGsm + function buyAsset( + uint256 minAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _buyAsset(msg.sender, minAmount, receiver); + } + + /// @inheritdoc IGsm + function buyAssetWithSig( + address originator, + uint256 minAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _buyAsset(originator, minAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAsset( + uint256 maxAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _sellAsset(msg.sender, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAssetWithSig( + address originator, + uint256 maxAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _sellAsset(originator, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function rescueTokens( + address token, + address to, + uint256 amount + ) external onlyRole(TOKEN_RESCUER_ROLE) { + require(amount > 0, 'INVALID_AMOUNT'); + if (token == GHO_TOKEN) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; + require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); + } + if (token == UNDERLYING_ASSET) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; + require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); + } + IERC20(token).safeTransfer(to, amount); + emit TokensRescued(token, to, amount); + } + + /// @inheritdoc IGsm + function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { + if (enable) { + require(!_isFrozen, 'GSM_ALREADY_FROZEN'); + } else { + require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); + } + _isFrozen = enable; + emit SwapFreeze(msg.sender, enable); + } + + /// @inheritdoc IGsm + function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + _isSeized = true; + _currentExposure = 0; + _updateExposureCap(0); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); + if (underlyingBalance > 0) { + IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); + } + + emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); + return underlyingBalance; + } + + /// @inheritdoc IGsm + function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + require(_isSeized, 'GSM_NOT_SEIZED'); + require(amount > 0, 'INVALID_AMOUNT'); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + if (amount > ghoMinted) { + amount = ghoMinted; + } + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); + IGhoToken(GHO_TOKEN).burn(amount); + + emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); + return amount; + } + + /// @inheritdoc IGsm + function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { + _updateFeeStrategy(feeStrategy); + } + + /// @inheritdoc IGsm + function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGhoFacilitator + function distributeFeesToTreasury() public virtual override { + uint256 accruedFees = _accruedFees; + if (accruedFees > 0) { + _accruedFees = 0; + IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); + emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); + } + } + + /// @inheritdoc IGhoFacilitator + function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { + _updateGhoTreasury(newGhoTreasury); + } + + /// @inheritdoc IGsm + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IGsm + function getGhoAmountForBuyAsset( + uint256 minAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAssetAmount); + return (assetAmount, ghoSold, grossAmount, fee); + } + + /// @inheritdoc IGsm + function getGhoAmountForSellAsset( + uint256 maxAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + return _calculateGhoAmountForSellAsset(maxAssetAmount); + } + + /// @inheritdoc IGsm + function getAssetAmountForBuyAsset( + uint256 maxGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) + : maxGhoAmount; + // round down so maxGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + true + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAssetAmountForSellAsset( + uint256 minGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(minGhoAmount) + : minGhoAmount; + // round up so minGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, true); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + false + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAvailableUnderlyingExposure() external view returns (uint256) { + return _exposureCap > _currentExposure ? _exposureCap - _currentExposure : 0; + } + + /// @inheritdoc IGsm + function getExposureCap() external view returns (uint128) { + return _exposureCap; + } + + /// @inheritdoc IGsm + function getAvailableLiquidity() external view returns (uint256) { + return _currentExposure; + } + + /// @inheritdoc IGsm + function getFeeStrategy() external view returns (address) { + return _feeStrategy; + } + + /// @inheritdoc IGsm + function getAccruedFees() external view returns (uint256) { + return _accruedFees; + } + + /// @inheritdoc IGsm + function getIsFrozen() external view returns (bool) { + return _isFrozen; + } + + /// @inheritdoc IGsm + function getIsSeized() external view returns (bool) { + return _isSeized; + } + + /// @inheritdoc IGsm + function canSwap() external view returns (bool) { + return !_isFrozen && !_isSeized; + } + + /// @inheritdoc IGhoFacilitator + function getGhoTreasury() external view override returns (address) { + return _ghoTreasury; + } + + /// @inheritdoc IGsm + function GSM_REVISION() public pure virtual override returns (uint256) { + return 2; + } + + /** + * @dev Buys an underlying asset with GHO + * @param originator The originator of the request + * @param minAmount The minimum amount of the underlying asset desired for purchase + * @param receiver The recipient address of the underlying asset being purchased + * @return The amount of underlying asset bought + * @return The amount of GHO sold by the user + */ + function _buyAsset( + address originator, + uint256 minAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAmount); + + _beforeBuyAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); + + _currentExposure -= uint128(assetAmount); + _accruedFees += fee.toUint128(); + // TRIGGER ERROR: transfer incorrect GHO amount to GSM + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold - 1); + IGhoToken(GHO_TOKEN).burn(grossAmount); + IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); + + emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); + return (assetAmount, ghoSold); + } + + /** + * @dev Hook that is called before `buyAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired for purchase + * @param receiver Recipient address of the underlying asset being purchased + */ + function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} + + /** + * @dev Sells an underlying asset for GHO + * @param originator The originator of the request + * @param maxAmount The maximum amount of the underlying asset desired to sell + * @param receiver The recipient address of the GHO being purchased + * @return The amount of underlying asset sold + * @return The amount of GHO bought by the user + */ + function _sellAsset( + address originator, + uint256 maxAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoBought, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForSellAsset(maxAmount); + + _beforeSellAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure + assetAmount <= _exposureCap, 'EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); + + _currentExposure += uint128(assetAmount); + _accruedFees += fee.toUint128(); + IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); + + IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); + IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); + + emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); + return (assetAmount, ghoBought); + } + + /** + * @dev Hook that is called before `sellAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired to sell + * @param receiver Recipient address of the GHO being purchased + */ + function _beforeSellAsset( + address originator, + uint256 amount, + address receiver + ) internal virtual {} + + /** + * @dev Returns the amount of GHO sold in exchange of buying underlying asset + * @param assetAmount The amount of underlying asset to buy + * @return The exact amount of asset the user purchases + * @return The total amount of GHO the user sells (gross amount in GHO plus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied on top of gross amount of GHO + */ + function _calculateGhoAmountForBuyAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the highest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; + uint256 ghoSold = grossAmount + fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) + : ghoSold; + // pick the lowest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + false + ); + uint256 finalFee = ghoSold - finalGrossAmount; + return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset + * @param assetAmount The amount of underlying asset to sell + * @return The exact amount of asset the user sells + * @return The total amount of GHO the user buys (gross amount in GHO minus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied to the gross amount of GHO + */ + function _calculateGhoAmountForSellAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the lowest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, false); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(grossAmount) : 0; + uint256 ghoBought = grossAmount - fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(ghoBought) + : ghoBought; + // pick the highest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + true + ); + uint256 finalFee = finalGrossAmount - ghoBought; + return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Updates Fee Strategy + * @param feeStrategy The address of the new Fee Strategy + */ + function _updateFeeStrategy(address feeStrategy) internal { + address oldFeeStrategy = _feeStrategy; + _feeStrategy = feeStrategy; + emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); + } + + /** + * @dev Updates Exposure Cap + * @param exposureCap The value of the new Exposure Cap + */ + function _updateExposureCap(uint128 exposureCap) internal { + uint128 oldExposureCap = _exposureCap; + _exposureCap = exposureCap; + emit ExposureCapUpdated(oldExposureCap, exposureCap); + } + + /** + * @dev Updates GHO Treasury Address + * @param newGhoTreasury The address of the new GHO Treasury + */ + function _updateGhoTreasury(address newGhoTreasury) internal { + require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); + address oldGhoTreasury = _ghoTreasury; + _ghoTreasury = newGhoTreasury; + emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return GSM_REVISION(); + } +} From 4f4603c93d0f14744b1d9fd6000e8bf6e510f95a Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 16:14:29 -0500 Subject: [PATCH 21/68] test: test coverage for remaining token balances --- src/test/TestGhoBase.t.sol | 13 +++ src/test/TestGsmConverter.t.sol | 98 ++++++++++++++++++- ...kRedemptionFailedRedeemableAssetAmount.sol | 65 ++++++++++++ ...ockRedemptionFailedRedeemedAssetAmount.sol | 65 ++++++++++++ 4 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol create mode 100644 src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 84a71452..7601e755 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -32,6 +32,8 @@ import {PriceOracle} from '@aave/core-v3/contracts/mocks/oracle/PriceOracle.sol' import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/TestnetERC20.sol'; import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; +import {MockRedemptionFailedRedeemableAssetAmount} from './mocks/MockRedemptionFailedRedeemableAssetAmount.sol'; +import {MockRedemptionFailedRedeemedAssetAmount} from './mocks/MockRedemptionFailedRedeemedAssetAmount.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -113,6 +115,8 @@ contract TestGhoBase is Test, Constants, Events { MockAddressesProvider PROVIDER; MockConfigurator CONFIGURATOR; MockRedemption BUIDL_USDC_REDEMPTION; + MockRedemptionFailedRedeemableAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT; + MockRedemptionFailedRedeemedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -342,6 +346,15 @@ contract TestGhoBase is Test, Constants, Events { GHO_STEWARD_V2.setControlledFacilitator(controlledFacilitators, true); BUIDL_USDC_REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); + BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT = new MockRedemptionFailedRedeemableAssetAmount( + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT = new MockRedemptionFailedRedeemedAssetAmount( + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + GSM_CONVERTER = new GsmConverter( address(this), address(GHO_BUIDL_GSM), diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index d2f2f43d..f790b1ae 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -109,6 +109,8 @@ contract TestGsmConverter is TestGhoBase { ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Buy assets via Redemption of USDC vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( @@ -170,6 +172,8 @@ contract TestGsmConverter is TestGhoBase { ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Buy assets via Redemption of USDC vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); emit BuyAssetThroughRedemption(BOB, CHARLES, expectedRedeemedAssetAmount, expectedGhoSold); (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( @@ -235,6 +239,8 @@ contract TestGsmConverter is TestGhoBase { // Supply assets to another user vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Buy assets via Redemption of USDC vm.expectRevert(stdError.arithmeticError); GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, CHARLES); vm.stopPrank(); @@ -253,7 +259,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(FAUCET); USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); - // Supply assets to another user + // Buy assets via Redemption of USDC vm.startPrank(BOB); vm.expectRevert(stdError.arithmeticError); GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, CHARLES); @@ -283,6 +289,8 @@ contract TestGsmConverter is TestGhoBase { ghoFaucet(BOB, expectedGhoSold + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + + // Buy assets via Redemption of USDC vm.expectRevert('INVALID_GHO_SOLD'); GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); vm.stopPrank(); @@ -292,8 +300,9 @@ contract TestGsmConverter is TestGhoBase { _upgradeToGsmFailedRemainingGhoBalance(); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); @@ -311,11 +320,94 @@ contract TestGsmConverter is TestGhoBase { ghoFaucet(BOB, expectedGhoSold + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + + // Buy assets via Redemption of USDC vm.expectRevert('INVALID_REMAINING_GHO_BALANCE'); GSM_CONVERTER.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); vm.stopPrank(); } + function testRevertBuyAssetInvalidRemainingRedeemableAssetBalance() public { + GsmConverter gsmConverter = new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint( + address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT), + DEFAULT_GSM_BUIDL_AMOUNT + ); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); + + // Buy assets via Redemption of USDC + vm.expectRevert('INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE'); + gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + vm.stopPrank(); + } + + function testRevertBuyAssetInvalidRemainingRedeemedAssetBalance() public { + GsmConverter gsmConverter = new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + uint256 bufferForAdditionalTransfer = 1000; + USDC_TOKEN.mint( + address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT), + DEFAULT_GSM_BUIDL_AMOUNT + bufferForAdditionalTransfer + ); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); + + // Buy assets via Redemption of USDC + vm.expectRevert('INVALID_REMAINING_REDEEMED_ASSET_BALANCE'); + gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + vm.stopPrank(); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); diff --git a/src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol b/src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol new file mode 100644 index 00000000..3e2c6e42 --- /dev/null +++ b/src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol @@ -0,0 +1,65 @@ +/** + * Copyright 2024 Circle Internet Financial, LTD. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * 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. + */ + +pragma solidity ^0.8.10; + +import {IRedemption} from '../../contracts/facilitators/gsm/dependencies/circle/IRedemption.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockRedemptionFailedRedeemableAssetAmount + * @dev Asset token is ERC20-compatible + * @dev Liquidity token is ERC20-compatible + */ +contract MockRedemptionFailedRedeemableAssetAmount is IRedemption { + using SafeERC20 for IERC20; + + /** + * @inheritdoc IRedemption + */ + address public immutable asset; + + /** + * @inheritdoc IRedemption + */ + address public immutable liquidity; + + /** + * @param _asset Address of asset token + * @param _liquidity Address of liquidity token + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @inheritdoc IRedemption + */ + function redeem(uint256 amount) external { + // mock outcome from Redemption + IERC20(asset).safeTransferFrom(msg.sender, address(this), amount - 1); + IERC20(liquidity).safeTransfer(msg.sender, amount); + } +} diff --git a/src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol b/src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol new file mode 100644 index 00000000..77983783 --- /dev/null +++ b/src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol @@ -0,0 +1,65 @@ +/** + * Copyright 2024 Circle Internet Financial, LTD. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * 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. + */ + +pragma solidity ^0.8.10; + +import {IRedemption} from '../../contracts/facilitators/gsm/dependencies/circle/IRedemption.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockRedemptionFailedRedeemedAssetAmount + * @dev Asset token is ERC20-compatible + * @dev Liquidity token is ERC20-compatible + */ +contract MockRedemptionFailedRedeemedAssetAmount is IRedemption { + using SafeERC20 for IERC20; + + /** + * @inheritdoc IRedemption + */ + address public immutable asset; + + /** + * @inheritdoc IRedemption + */ + address public immutable liquidity; + + /** + * @param _asset Address of asset token + * @param _liquidity Address of liquidity token + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @inheritdoc IRedemption + */ + function redeem(uint256 amount) external { + // mock outcome from Redemption + IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); + IERC20(liquidity).safeTransfer(msg.sender, amount + 1); + } +} From 1dbff49f769e4903a1e395854b2af9ba4ef5fde3 Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 16:24:19 -0500 Subject: [PATCH 22/68] feat: init buyAssetWithSig --- .../gsm/converter/GsmConverter.sol | 78 +++++++++++++++---- .../converter/interfaces/IGsmConverter.sol | 42 ++++++++-- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 641dc1f9..3e4b001d 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.10; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; @@ -14,9 +16,15 @@ import {IRedemption} from '../dependencies/circle/IRedemption.sol'; * @author Aave * @notice Converter that facilitates conversions/redemptions of underlying assets. Integrates with GSM to buy/sell to go to/from an underlying asset to/from GHO. */ -contract GsmConverter is Ownable, IGsmConverter { +contract GsmConverter is Ownable, EIP712, IGsmConverter { using GPv2SafeERC20 for IERC20; + /// @inheritdoc IGsmConverter + bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + /// @inheritdoc IGsmConverter address public immutable GHO_TOKEN; @@ -32,6 +40,9 @@ contract GsmConverter is Ownable, IGsmConverter { /// @inheritdoc IGsmConverter address public immutable REDEMPTION_CONTRACT; + /// @inheritdoc IGsmConverter + mapping(address => uint256) public nonces; + /** * @dev Constructor * @param gsm The address of the associated GSM contract @@ -45,7 +56,7 @@ contract GsmConverter is Ownable, IGsmConverter { address redemptionContract, address redeemableAsset, address redeemedAsset - ) { + ) EIP712('GSMConverter', '1') { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); @@ -65,13 +76,59 @@ contract GsmConverter is Ownable, IGsmConverter { function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); + return _buyAsset(msg.sender, minAmount, receiver); + } + + /// @inheritdoc IGsmConverter + function buyAssetWithSig( + address originator, + uint256 minAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _buyAsset(originator, minAmount, receiver); + } + + // TODO: + // 2) implement sellAsset (sell USDC -> get GHO) + // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently + // - send BUIDL to GSM, get GHO from GSM + // - send GHO to user, safeTransfer + + /// @inheritdoc IGsmConverter + function rescueTokens(address token, address to, uint256 amount) external onlyOwner { + require(amount > 0, 'INVALID_AMOUNT'); + IERC20(token).safeTransfer(to, amount); + emit TokensRescued(token, to, amount); + } + + function _buyAsset( + address originator, + uint256 minAmount, + address receiver + ) internal returns (uint256, uint256) { uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); uint256 initialRedeemableAssetBalance = IERC20(REDEEMABLE_ASSET).balanceOf(address(this)); uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); - IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoAmount); + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); @@ -97,20 +154,7 @@ contract GsmConverter is Ownable, IGsmConverter { 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' ); - emit BuyAssetThroughRedemption(msg.sender, receiver, redeemableAssetAmount, ghoSold); + emit BuyAssetThroughRedemption(originator, receiver, redeemableAssetAmount, ghoSold); return (redeemableAssetAmount, ghoSold); } - - // TODO: - // 2) implement sellAsset (sell USDC -> get GHO) - // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently - // - send BUIDL to GSM, get GHO from GSM - // - send GHO to user, safeTransfer - - /// @inheritdoc IGsmConverter - function rescueTokens(address token, address to, uint256 amount) external onlyOwner { - require(amount > 0, 'INVALID_AMOUNT'); - IERC20(token).safeTransfer(to, amount); - emit TokensRescued(token, to, amount); - } } diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 0da148f7..c7e4b008 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -44,12 +44,23 @@ interface IGsmConverter { function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256); /** - * @notice Rescue and transfer tokens locked in this contract - * @param token The address of the token - * @param to The address of the recipient - * @param amount The amount of token to transfer + * @notice Buys the GSM underlying asset in exchange for selling GHO after asset redemption, using an EIP-712 signature + * @dev Use `getAssetAmountForBuyAsset` function to calculate the amount based on the GHO amount to sell + * @param originator The signer of the request + * @param minAmount The minimum amount of the underlying asset to buy + * @param receiver Recipient address of the underlying asset being purchased + * @param deadline Signature expiration deadline + * @param signature Signature data + * @return The amount of underlying asset bought + * @return The amount of GHO sold by the user */ - function rescueTokens(address token, address to, uint256 amount) external; + function buyAssetWithSig( + address originator, + uint256 minAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external returns (uint256, uint256); /** * @notice Sells the GSM underlying asset in exchange for buying GHO, after asset conversion @@ -60,6 +71,14 @@ interface IGsmConverter { */ // function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); + /** + * @notice Rescue and transfer tokens locked in this contract + * @param token The address of the token + * @param to The address of the recipient + * @param amount The amount of token to transfer + */ + function rescueTokens(address token, address to, uint256 amount) external; + /** * @notice Returns the address of the GHO token * @return The address of GHO token contract @@ -89,4 +108,17 @@ interface IGsmConverter { * @return The address of the redemption contract */ function REDEMPTION_CONTRACT() external view returns (address); + + /** + * @notice Returns the current nonce (for EIP-712 signature methods) of an address + * @param user The address of the user + * @return The current nonce of the user + */ + function nonces(address user) external view returns (uint256); + + /** + * @notice Returns the EIP-712 signature typehash for buyAssetWithSig + * @return The bytes32 signature typehash for buyAssetWithSig + */ + function BUY_ASSET_WITH_SIG_TYPEHASH() external pure returns (bytes32); } From bce34e7d605fb86ce7e394cc6f154d2e6efdbb03 Mon Sep 17 00:00:00 2001 From: YBM Date: Fri, 6 Sep 2024 19:00:44 -0500 Subject: [PATCH 23/68] test: happy path test for buyAssetWithSig --- .../gsm/converter/GsmConverter.sol | 14 +++ .../converter/interfaces/IGsmConverter.sol | 6 + src/test/TestGsmConverter.t.sol | 115 +++++++++++++++++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 3e4b001d..8812ab0d 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -11,6 +11,8 @@ import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; +import 'forge-std/console2.sol'; + /** * @title GsmConverter * @author Aave @@ -117,6 +119,18 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { emit TokensRescued(token, to, amount); } + /// @inheritdoc IGsmConverter + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Buys the GSM underlying asset in exchange for selling GHO, after asset redemption + * @param minAmount The minimum amount of the underlying asset to buy (ie BUIDL) + * @param receiver Recipient address of the underlying asset being purchased + * @return The amount of underlying asset bought, after asset redemption + * @return The amount of GHO sold by the user + */ function _buyAsset( address originator, uint256 minAmount, diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index c7e4b008..1add56a2 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -116,6 +116,12 @@ interface IGsmConverter { */ function nonces(address user) external view returns (uint256); + /** + * @notice Returns the EIP712 domain separator + * @return The EIP712 domain separator + */ + function DOMAIN_SEPARATOR() external view returns (bytes32); + /** * @notice Returns the EIP-712 signature typehash for buyAssetWithSig * @return The bytes32 signature typehash for buyAssetWithSig diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index f790b1ae..fdad4238 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -7,9 +7,10 @@ contract TestGsmConverter is TestGhoBase { // using PercentageMath for uint256; // using PercentageMath for uint128; - function setUp() public { - // (gsmSignerAddr, gsmSignerKey) = makeAddrAndKey('gsmSigner'); - } + address gsmConverterSignerAddr; + uint256 gsmConverterSignerKey; + + function setUp() public {} function testConstructor() public { GsmConverter gsmConverter = new GsmConverter( @@ -408,6 +409,114 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } + function testConverterBuyAssetWithSig() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('asdf'); + vm.assume(gsmConverterSignerAddr != ALICE && gsmConverterSignerAddr != BOB); + + uint256 deadline = block.timestamp + 1 hours; + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.startPrank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedRedeemedAssetAmount, + expectedGhoSold + ); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + DEFAULT_GSM_BUIDL_AMOUNT, + 'Unexpected buyer final USDC balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + 0, + 'Unexpected buyer final GHO balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected buyer final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final BUIDL balance' + ); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From 62657eeb626b109d89dcd90e2a3f658d537c2117 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 10:35:26 -0500 Subject: [PATCH 24/68] test: simplify testBuyAssetWithSig, add testBuyAssetWithSigExactDeadline --- src/test/TestGsmConverter.t.sol | 231 +++++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 4 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index fdad4238..c76287de 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -409,9 +409,8 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } - function testConverterBuyAssetWithSig() public { - (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('asdf'); - vm.assume(gsmConverterSignerAddr != ALICE && gsmConverterSignerAddr != BOB); + function testBuyAssetWithSig() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); uint256 deadline = block.timestamp + 1 hours; @@ -433,7 +432,7 @@ contract TestGsmConverter is TestGhoBase { // Supply assets to another user ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); - vm.startPrank(gsmConverterSignerAddr); + vm.prank(gsmConverterSignerAddr); GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); assertEq( @@ -459,6 +458,9 @@ contract TestGsmConverter is TestGhoBase { (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); // Buy assets via Redemption of USDC vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); emit BuyAssetThroughRedemption( @@ -474,8 +476,117 @@ contract TestGsmConverter is TestGhoBase { deadline, signature ); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + DEFAULT_GSM_BUIDL_AMOUNT, + 'Unexpected buyer final USDC balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + 0, + 'Unexpected buyer final GHO balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected buyer final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final BUIDL balance' + ); + } + + function testBuyAssetWithSigExactDeadline() public { + // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp; + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedRedeemedAssetAmount, + expectedGhoSold + ); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); assertEq( @@ -517,6 +628,118 @@ contract TestGsmConverter is TestGhoBase { ); } + // function testFuzzBuyAssetWithSig(string memory randomString) public { + // (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey(randomString); + // vm.assume( + // gsmConverterSignerAddr != ALICE && + // gsmConverterSignerAddr != BOB && + // gsmConverterSignerAddr != address(0) + // ); + + // uint256 deadline = block.timestamp + 1 hours; + + // uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + // .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // // Supply BUIDL assets to the BUIDL GSM first + // vm.prank(FAUCET); + // BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + // vm.startPrank(ALICE); + // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + // GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + // vm.stopPrank(); + + // // Supply USDC to the Redemption contract + // vm.prank(FAUCET); + // USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // // Supply assets to another user + // ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + // vm.startPrank(gsmConverterSignerAddr); + // GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // assertEq( + // GSM_CONVERTER.nonces(gsmConverterSignerAddr), + // 0, + // 'Unexpected before gsmConverterSignerAddr nonce' + // ); + + // bytes32 digest = keccak256( + // abi.encode( + // '\x19\x01', + // GSM_CONVERTER.DOMAIN_SEPARATOR(), + // GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + // abi.encode( + // gsmConverterSignerAddr, + // DEFAULT_GSM_BUIDL_AMOUNT, + // gsmConverterSignerAddr, + // GSM_CONVERTER.nonces(gsmConverterSignerAddr), + // deadline + // ) + // ) + // ); + // (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + // bytes memory signature = abi.encodePacked(r, s, v); + + // // Buy assets via Redemption of USDC + // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + // emit BuyAssetThroughRedemption( + // gsmConverterSignerAddr, + // gsmConverterSignerAddr, + // expectedRedeemedAssetAmount, + // expectedGhoSold + // ); + // (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( + // gsmConverterSignerAddr, + // DEFAULT_GSM_BUIDL_AMOUNT, + // gsmConverterSignerAddr, + // deadline, + // signature + // ); + // vm.stopPrank(); + + // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + // assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); + // assertEq( + // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + // DEFAULT_GSM_BUIDL_AMOUNT, + // 'Unexpected buyer final USDC balance' + // ); + // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + // assertEq( + // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final USDC balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + // 0, + // 'Unexpected buyer final GHO balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, + // 'Unexpected GSM final GHO balance' + // ); + // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + // assertEq( + // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + // 0, + // 'Unexpected buyer final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // 0, + // 'Unexpected GSM final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final BUIDL balance' + // ); + // } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From 522ada9348e0e2543356d5e040c90d5bf698506f Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 10:38:54 -0500 Subject: [PATCH 25/68] test: add testRevertBuyAssetWithSigExpiredSignature --- src/test/TestGsmConverter.t.sol | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index c76287de..f281e21c 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -740,6 +740,69 @@ contract TestGsmConverter is TestGhoBase { // ); // } + function testRevertBuyAssetWithSigExpiredSignature() public { + // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp - 1; + + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + vm.expectRevert('SIGNATURE_DEADLINE_EXPIRED'); + GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From b60751d3b373c1af36fc179aef810b9e14fcc9c7 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 10:42:29 -0500 Subject: [PATCH 26/68] test: add testRevertBuyAssetWithSigInvalidSignature --- src/test/TestGsmConverter.t.sol | 63 +++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index f281e21c..c980c6e4 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -745,10 +745,7 @@ contract TestGsmConverter is TestGhoBase { (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); uint256 deadline = block.timestamp - 1; - uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); @@ -803,6 +800,66 @@ contract TestGsmConverter is TestGhoBase { ); } + function testRevertBuyAssetWithSigInvalidSignature() public { + // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 1; + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.buyAssetWithSig( + BOB, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From 6d90a66a720f3ba6bffcc7ae3615642d74cfed4d Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 10:44:11 -0500 Subject: [PATCH 27/68] test: add testRevertBuyAssetWithSigInvalidAmount --- src/test/TestGsmConverter.t.sol | 62 ++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index c980c6e4..0b7b8574 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -852,7 +852,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(BOB); vm.expectRevert('SIGNATURE_INVALID'); GSM_CONVERTER.buyAssetWithSig( - BOB, + BOB, // invalid signer DEFAULT_GSM_BUIDL_AMOUNT, gsmConverterSignerAddr, deadline, @@ -860,6 +860,66 @@ contract TestGsmConverter is TestGhoBase { ); } + function testRevertBuyAssetWithSigInvalidAmount() public { + // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 1; + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT + 1, // invalid amount + gsmConverterSignerAddr, + deadline, + signature + ); + } + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From ea90912246238ee7eb5e2b612da9b6bae145cb6c Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 12:26:28 -0500 Subject: [PATCH 28/68] test: refer to proper value for BUIDL gsm exposure --- src/test/TestGhoBase.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 7601e755..0c66bbac 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -304,7 +304,7 @@ contract TestGhoBase is Test, Constants, Events { '' ); GHO_BUIDL_GSM = Gsm(address(buidlGsmProxy)); - GHO_BUIDL_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + GHO_BUIDL_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_BUIDL_EXPOSURE); GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); From c9140c51df7e1042176a7c4fa70197a693d77981 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 12:27:06 -0500 Subject: [PATCH 29/68] test: resolve test assertion on remaining gsm gho balance --- src/test/TestGsmConverter.t.sol | 253 ++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 113 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 0b7b8574..cde7620b 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -7,8 +7,8 @@ contract TestGsmConverter is TestGhoBase { // using PercentageMath for uint256; // using PercentageMath for uint128; - address gsmConverterSignerAddr; - uint256 gsmConverterSignerKey; + address public gsmConverterSignerAddr; + uint256 public gsmConverterSignerKey; function setUp() public {} @@ -628,117 +628,144 @@ contract TestGsmConverter is TestGhoBase { ); } - // function testFuzzBuyAssetWithSig(string memory randomString) public { - // (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey(randomString); - // vm.assume( - // gsmConverterSignerAddr != ALICE && - // gsmConverterSignerAddr != BOB && - // gsmConverterSignerAddr != address(0) - // ); - - // uint256 deadline = block.timestamp + 1 hours; - - // uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM - // .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); - - // // Supply BUIDL assets to the BUIDL GSM first - // vm.prank(FAUCET); - // BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); - // vm.startPrank(ALICE); - // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); - // GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); - // vm.stopPrank(); - - // // Supply USDC to the Redemption contract - // vm.prank(FAUCET); - // USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); - - // // Supply assets to another user - // ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); - // vm.startPrank(gsmConverterSignerAddr); - // GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); - - // assertEq( - // GSM_CONVERTER.nonces(gsmConverterSignerAddr), - // 0, - // 'Unexpected before gsmConverterSignerAddr nonce' - // ); - - // bytes32 digest = keccak256( - // abi.encode( - // '\x19\x01', - // GSM_CONVERTER.DOMAIN_SEPARATOR(), - // GSM_BUY_ASSET_WITH_SIG_TYPEHASH, - // abi.encode( - // gsmConverterSignerAddr, - // DEFAULT_GSM_BUIDL_AMOUNT, - // gsmConverterSignerAddr, - // GSM_CONVERTER.nonces(gsmConverterSignerAddr), - // deadline - // ) - // ) - // ); - // (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); - // bytes memory signature = abi.encodePacked(r, s, v); - - // // Buy assets via Redemption of USDC - // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - // emit BuyAssetThroughRedemption( - // gsmConverterSignerAddr, - // gsmConverterSignerAddr, - // expectedRedeemedAssetAmount, - // expectedGhoSold - // ); - // (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( - // gsmConverterSignerAddr, - // DEFAULT_GSM_BUIDL_AMOUNT, - // gsmConverterSignerAddr, - // deadline, - // signature - // ); - // vm.stopPrank(); - - // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - // assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); - // assertEq( - // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), - // DEFAULT_GSM_BUIDL_AMOUNT, - // 'Unexpected buyer final USDC balance' - // ); - // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); - // assertEq( - // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final USDC balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), - // 0, - // 'Unexpected buyer final GHO balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, - // 'Unexpected GSM final GHO balance' - // ); - // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); - // assertEq( - // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), - // 0, - // 'Unexpected buyer final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // 0, - // 'Unexpected GSM final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final BUIDL balance' - // ); - // } + function _printTest(uint256 gsmGhoAmount) private returns (uint256, uint256, uint256) { + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(gsmGhoAmount); + (, , , uint256 sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(gsmGhoAmount); + return (expectedGhoSold, buyFee, sell); + } + + function testFuzzBuyAssetWithSig( + // string memory randomString, + // uint256 deadlineBuffer, + uint256 gsmGhoAmount + ) public { + // deadlineBuffer = bound(deadlineBuffer, 0, 1e18); + gsmGhoAmount = bound(gsmGhoAmount, 1, GHO_BUIDL_GSM.getExposureCap()); + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + vm.assume( + gsmConverterSignerAddr != ALICE && + gsmConverterSignerAddr != BOB && + gsmConverterSignerAddr != address(0) + ); + + uint256 deadline = block.timestamp + 100; + + // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM + // .getGhoAmountForBuyAsset(gsmGhoAmount); + + // (, , , uint256 sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(gsmGhoAmount); + + (uint expectedGhoSold, uint buyFee, uint sell) = _printTest(gsmGhoAmount); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, gsmGhoAmount); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), gsmGhoAmount); + GHO_BUIDL_GSM.sellAsset(gsmGhoAmount, ALICE); + vm.stopPrank(); + + // _printTest(gsmGhoAmount); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), gsmGhoAmount); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, expectedGhoSold); + vm.startPrank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); + + // console.log('ghoamt', gsmGhoAmount, buyFee, expectedGhoSold); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + gsmConverterSignerKey, + keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + gsmGhoAmount, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ) + ); + bytes memory signature = abi.encodePacked(r, s, v); + + // Buy assets via Redemption of USDC + // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + // emit BuyAssetThroughRedemption( + // gsmConverterSignerAddr, + // gsmConverterSignerAddr, + // expectedRedeemedAssetAmount, + // expectedGhoSold + // ); + GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + gsmGhoAmount, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + + // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + // assertEq(redeemedUSDCAmount, gsmGhoAmount, 'Unexpected redeemed buyAsset amount'); + // assertEq( + // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + // gsmGhoAmount, + // 'Unexpected buyer final USDC balance' + // ); + // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + // assertEq( + // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final USDC balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + // 0, + // 'Unexpected buyer final GHO balance' + // ); + // console.log( + // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(ghoSold), + // buyFee + // ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sell + buyFee, + 'Unexpected GSM final GHO balance' + ); + // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM Converter final GHO balance'); + // assertEq( + // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + // 0, + // 'Unexpected buyer final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // 0, + // 'Unexpected GSM final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final BUIDL balance' + // ); + } function testRevertBuyAssetWithSigExpiredSignature() public { // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp From 14fc942abe3193fd7e30d6edfe7681d984900e1e Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 14:23:35 -0500 Subject: [PATCH 30/68] test: clean up assertion failure messages, reference different test variables --- src/test/TestGsmConverter.t.sol | 289 +++++++++++++++----------------- 1 file changed, 139 insertions(+), 150 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index cde7620b..05aa7cb2 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -104,7 +104,7 @@ contract TestGsmConverter is TestGhoBase { // Supply USDC to the Redemption contract vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedUSDCAmount); // Supply assets to another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); @@ -135,7 +135,11 @@ contract TestGsmConverter is TestGhoBase { sellFee + buyFee, 'Unexpected GSM final GHO balance' ); - assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); assertEq( BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), @@ -145,7 +149,7 @@ contract TestGsmConverter is TestGhoBase { assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected converter final BUIDL balance' + 'Unexpected GSM_CONVERTER final BUIDL balance' ); } @@ -167,7 +171,7 @@ contract TestGsmConverter is TestGhoBase { // Supply USDC to the Redemption contract vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedUSDCAmount); // Supply assets to another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); @@ -202,7 +206,11 @@ contract TestGsmConverter is TestGhoBase { sellFee + buyFee, 'Unexpected GSM final GHO balance' ); - assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); assertEq( BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), @@ -212,7 +220,7 @@ contract TestGsmConverter is TestGhoBase { assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected converter final BUIDL balance' + 'Unexpected GSM_CONVERTER final BUIDL balance' ); } @@ -271,8 +279,9 @@ contract TestGsmConverter is TestGhoBase { _upgradeToGsmFailedGhoAmount(); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); @@ -500,7 +509,11 @@ contract TestGsmConverter is TestGhoBase { GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, 'Unexpected GSM final GHO balance' ); - assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); assertEq( BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), 0, @@ -514,7 +527,7 @@ contract TestGsmConverter is TestGhoBase { assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected converter final BUIDL balance' + 'Unexpected GSM_CONVERTER final BUIDL balance' ); } @@ -610,7 +623,11 @@ contract TestGsmConverter is TestGhoBase { GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, 'Unexpected GSM final GHO balance' ); - assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); assertEq( BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), 0, @@ -624,148 +641,120 @@ contract TestGsmConverter is TestGhoBase { assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected converter final BUIDL balance' + 'Unexpected GSM_CONVERTER final BUIDL balance' ); } - function _printTest(uint256 gsmGhoAmount) private returns (uint256, uint256, uint256) { - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(gsmGhoAmount); - (, , , uint256 sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(gsmGhoAmount); - return (expectedGhoSold, buyFee, sell); - } - - function testFuzzBuyAssetWithSig( - // string memory randomString, - // uint256 deadlineBuffer, - uint256 gsmGhoAmount - ) public { - // deadlineBuffer = bound(deadlineBuffer, 0, 1e18); - gsmGhoAmount = bound(gsmGhoAmount, 1, GHO_BUIDL_GSM.getExposureCap()); - (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); - vm.assume( - gsmConverterSignerAddr != ALICE && - gsmConverterSignerAddr != BOB && - gsmConverterSignerAddr != address(0) - ); - - uint256 deadline = block.timestamp + 100; - - // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM - // .getGhoAmountForBuyAsset(gsmGhoAmount); - - // (, , , uint256 sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(gsmGhoAmount); - - (uint expectedGhoSold, uint buyFee, uint sell) = _printTest(gsmGhoAmount); - - // Supply BUIDL assets to the BUIDL GSM first - vm.prank(FAUCET); - BUIDL_TOKEN.mint(ALICE, gsmGhoAmount); - vm.startPrank(ALICE); - BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), gsmGhoAmount); - GHO_BUIDL_GSM.sellAsset(gsmGhoAmount, ALICE); - vm.stopPrank(); - - // _printTest(gsmGhoAmount); - - // Supply USDC to the Redemption contract - vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), gsmGhoAmount); - - // Supply assets to another user - ghoFaucet(gsmConverterSignerAddr, expectedGhoSold); - vm.startPrank(gsmConverterSignerAddr); - GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); - - // console.log('ghoamt', gsmGhoAmount, buyFee, expectedGhoSold); - - assertEq( - GSM_CONVERTER.nonces(gsmConverterSignerAddr), - 0, - 'Unexpected before gsmConverterSignerAddr nonce' - ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - gsmConverterSignerKey, - keccak256( - abi.encode( - '\x19\x01', - GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, - abi.encode( - gsmConverterSignerAddr, - gsmGhoAmount, - gsmConverterSignerAddr, - GSM_CONVERTER.nonces(gsmConverterSignerAddr), - deadline - ) - ) - ) - ); - bytes memory signature = abi.encodePacked(r, s, v); - - // Buy assets via Redemption of USDC - // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - // emit BuyAssetThroughRedemption( - // gsmConverterSignerAddr, - // gsmConverterSignerAddr, - // expectedRedeemedAssetAmount, - // expectedGhoSold - // ); - GSM_CONVERTER.buyAssetWithSig( - gsmConverterSignerAddr, - gsmGhoAmount, - gsmConverterSignerAddr, - deadline, - signature - ); - vm.stopPrank(); - - // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - // assertEq(redeemedUSDCAmount, gsmGhoAmount, 'Unexpected redeemed buyAsset amount'); - // assertEq( - // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), - // gsmGhoAmount, - // 'Unexpected buyer final USDC balance' - // ); - // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); - // assertEq( - // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final USDC balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), - // 0, - // 'Unexpected buyer final GHO balance' - // ); - // console.log( - // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(ghoSold), - // buyFee - // ); - assertEq( - GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - sell + buyFee, - 'Unexpected GSM final GHO balance' - ); - // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM Converter final GHO balance'); - // assertEq( - // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), - // 0, - // 'Unexpected buyer final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // 0, - // 'Unexpected GSM final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final BUIDL balance' - // ); - } + // function testFuzzMinAmountBuyAssetWithSig(uint256 minAmount) public { + // minAmount = bound(minAmount, 1, DEFAULT_GSM_BUIDL_AMOUNT); + // // minAmount = DEFAULT_GSM_BUIDL_AMOUNT; + + // (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + // uint256 deadline = block.timestamp + 1 hours; + + // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM + // .getGhoAmountForBuyAsset(minAmount); + + // // Supply BUIDL assets to the BUIDL GSM first + // vm.prank(FAUCET); + // BUIDL_TOKEN.mint(ALICE, minAmount); + // vm.startPrank(ALICE); + // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), minAmount); + // GHO_BUIDL_GSM.sellAsset(minAmount, ALICE); + // vm.stopPrank(); + + // // Supply USDC to the Redemption contract + // vm.prank(FAUCET); + // USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), minAmount); + + // // Supply assets to another user + // ghoFaucet(gsmConverterSignerAddr, expectedGhoSold + buyFee); + // vm.prank(gsmConverterSignerAddr); + // GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); + + // assertEq( + // GSM_CONVERTER.nonces(gsmConverterSignerAddr), + // 0, + // 'Unexpected before gsmConverterSignerAddr nonce' + // ); + + // bytes32 digest = keccak256( + // abi.encode( + // '\x19\x01', + // GSM_CONVERTER.DOMAIN_SEPARATOR(), + // GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + // abi.encode( + // gsmConverterSignerAddr, + // minAmount, + // gsmConverterSignerAddr, + // GSM_CONVERTER.nonces(gsmConverterSignerAddr), + // deadline + // ) + // ) + // ); + // (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + // bytes memory signature = abi.encodePacked(r, s, v); + + // assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + // vm.prank(BOB); + // // Buy assets via Redemption of USDC + // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + // emit BuyAssetThroughRedemption( + // gsmConverterSignerAddr, + // gsmConverterSignerAddr, + // expectedRedeemedAssetAmount, + // expectedGhoSold + // ); + // (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( + // gsmConverterSignerAddr, + // minAmount, + // gsmConverterSignerAddr, + // deadline, + // signature + // ); + + // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + // assertEq(redeemedUSDCAmount, minAmount, 'Unexpected redeemed buyAsset amount'); + // assertEq( + // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + // minAmount, + // 'Unexpected buyer final USDC balance' + // ); + // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + // assertEq( + // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final USDC balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + // 0, + // 'Unexpected buyer final GHO balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, + // 'Unexpected GSM final GHO balance' + // ); + // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + // assertEq( + // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + // 0, + // 'Unexpected buyer final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // 0, + // 'Unexpected GSM final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final BUIDL balance' + // ); + // } function testRevertBuyAssetWithSigExpiredSignature() public { // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp From 0ba7d24e5324cd6c599fe4ee02e99b8e465ec395 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 15:50:51 -0500 Subject: [PATCH 31/68] refactor: code segmentation --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 8812ab0d..432edd59 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -144,11 +144,10 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); - (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); - IGhoToken(GHO_TOKEN).approve(address(GSM), 0); + IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), 0); From 1d0e3264ee6f573435765b7fdfe041f0ba6cd665 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 9 Sep 2024 15:51:14 -0500 Subject: [PATCH 32/68] test: fuzz test on minAssetAmount values --- src/test/TestGsmConverter.t.sol | 230 +++++++++++++++++--------------- 1 file changed, 120 insertions(+), 110 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 05aa7cb2..43686876 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -645,116 +645,126 @@ contract TestGsmConverter is TestGhoBase { ); } - // function testFuzzMinAmountBuyAssetWithSig(uint256 minAmount) public { - // minAmount = bound(minAmount, 1, DEFAULT_GSM_BUIDL_AMOUNT); - // // minAmount = DEFAULT_GSM_BUIDL_AMOUNT; - - // (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); - - // uint256 deadline = block.timestamp + 1 hours; - - // (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM - // .getGhoAmountForBuyAsset(minAmount); - - // // Supply BUIDL assets to the BUIDL GSM first - // vm.prank(FAUCET); - // BUIDL_TOKEN.mint(ALICE, minAmount); - // vm.startPrank(ALICE); - // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), minAmount); - // GHO_BUIDL_GSM.sellAsset(minAmount, ALICE); - // vm.stopPrank(); - - // // Supply USDC to the Redemption contract - // vm.prank(FAUCET); - // USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), minAmount); - - // // Supply assets to another user - // ghoFaucet(gsmConverterSignerAddr, expectedGhoSold + buyFee); - // vm.prank(gsmConverterSignerAddr); - // GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold + buyFee); - - // assertEq( - // GSM_CONVERTER.nonces(gsmConverterSignerAddr), - // 0, - // 'Unexpected before gsmConverterSignerAddr nonce' - // ); - - // bytes32 digest = keccak256( - // abi.encode( - // '\x19\x01', - // GSM_CONVERTER.DOMAIN_SEPARATOR(), - // GSM_BUY_ASSET_WITH_SIG_TYPEHASH, - // abi.encode( - // gsmConverterSignerAddr, - // minAmount, - // gsmConverterSignerAddr, - // GSM_CONVERTER.nonces(gsmConverterSignerAddr), - // deadline - // ) - // ) - // ); - // (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); - // bytes memory signature = abi.encodePacked(r, s, v); - - // assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); - - // vm.prank(BOB); - // // Buy assets via Redemption of USDC - // vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - // emit BuyAssetThroughRedemption( - // gsmConverterSignerAddr, - // gsmConverterSignerAddr, - // expectedRedeemedAssetAmount, - // expectedGhoSold - // ); - // (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAssetWithSig( - // gsmConverterSignerAddr, - // minAmount, - // gsmConverterSignerAddr, - // deadline, - // signature - // ); - - // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - // assertEq(redeemedUSDCAmount, minAmount, 'Unexpected redeemed buyAsset amount'); - // assertEq( - // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), - // minAmount, - // 'Unexpected buyer final USDC balance' - // ); - // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); - // assertEq( - // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final USDC balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), - // 0, - // 'Unexpected buyer final GHO balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, - // 'Unexpected GSM final GHO balance' - // ); - // assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); - // assertEq( - // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), - // 0, - // 'Unexpected buyer final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // 0, - // 'Unexpected GSM final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final BUIDL balance' - // ); - // } + function _getBuySellFees( + uint256 amount + ) + private + returns ( + uint256 expectedRedeemedAssetAmount, + uint256 expectedGhoSold, + uint256 buyFee, + uint256 sell + ) + { + (expectedRedeemedAssetAmount, expectedGhoSold, , buyFee) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(amount); + (, , , sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(amount); + } + + function testFuzzMinAmountBuyAssetWithSig(uint minAssetAmount) public { + minAssetAmount = bound(minAssetAmount, 1, DEFAULT_GSM_BUIDL_AMOUNT * 1000); + + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 1 hours; + ( + uint256 expectedRedeemedAssetAmount, + uint256 expectedGhoSold, + uint256 buyFee, + uint256 sellFee + ) = _getBuySellFees(minAssetAmount); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, minAssetAmount); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), minAssetAmount); + GHO_BUIDL_GSM.sellAsset(minAssetAmount, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), minAssetAmount); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, expectedGhoSold); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + minAssetAmount, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedRedeemedAssetAmount, + expectedGhoSold + ); + GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + minAssetAmount, + gsmConverterSignerAddr, + deadline, + signature + ); + + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + minAssetAmount, + 'Unexpected buyer final USDC balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + 0, + 'Unexpected buyer final GHO balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, 'Unexpected GSM final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected buyer final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + } function testRevertBuyAssetWithSigExpiredSignature() public { // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp From fdd0046753d194d5ccef0a872402f86f25092e59 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 11:54:48 -0500 Subject: [PATCH 33/68] feat: init sellAsset functionality with mock --- .../gsm/converter/GsmConverter.sol | 47 +++++++++++++++++-- .../converter/interfaces/IGsmConverter.sol | 8 +++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 432edd59..1c94956f 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -42,6 +42,9 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { /// @inheritdoc IGsmConverter address public immutable REDEMPTION_CONTRACT; + /// @inheritdoc IGsmConverter + address public immutable ONRAMP_CONTRACT; + /// @inheritdoc IGsmConverter mapping(address => uint256) public nonces; @@ -56,17 +59,20 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address admin, address gsm, address redemptionContract, + address onrampContract, address redeemableAsset, address redeemedAsset ) EIP712('GSMConverter', '1') { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(onrampContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); GSM = gsm; REDEMPTION_CONTRACT = redemptionContract; + ONRAMP_CONTRACT = onrampContract; REDEEMABLE_ASSET = redeemableAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); @@ -106,11 +112,12 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { return _buyAsset(originator, minAmount, receiver); } - // TODO: - // 2) implement sellAsset (sell USDC -> get GHO) - // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently - // - send BUIDL to GSM, get GHO from GSM - // - send GHO to user, safeTransfer + /// @inheritdoc IGsmConverter + function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256) { + require(minAmount > 0, 'INVALID_MIN_AMOUNT'); + + return _sellAsset(msg.sender, maxAmount, receiver); + } /// @inheritdoc IGsmConverter function rescueTokens(address token, address to, uint256 amount) external onlyOwner { @@ -170,4 +177,34 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { emit BuyAssetThroughRedemption(originator, receiver, redeemableAssetAmount, ghoSold); return (redeemableAssetAmount, ghoSold); } + + /** + * @notice Sells the GSM underlying asset in exchange for buying GHO, after asset conversion + * @param originator The originator of the request + * @param maxAmount The maximum amount of the underlying asset to sell + * @param receiver Recipient address of the GHO being purchased + * @return The amount of underlying asset sold, after asset conversion + * @return The amount of GHO bought by the user + */ + function _sellAsset( + address originator, + uint256 maxAmount, + address receiver + ) internal returns (uint256, uint256) { + // TODO: + // 2) implement sellAsset (sell USDC -> get GHO) + // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently + // - send BUIDL to GSM, get GHO from GSM + // - send GHO to user, safeTransfer + + ( + uint256 assetAmount, + uint256 ghoBought, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForSellAsset(maxAmount); + + // IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), assetAmount); + // IERC20(REDEEMED_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); + } } diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 1add56a2..e679ae2d 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -69,7 +69,7 @@ interface IGsmConverter { * @return The amount of underlying asset sold, after asset conversion * @return The amount of GHO bought by the user */ - // function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); + function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); /** * @notice Rescue and transfer tokens locked in this contract @@ -109,6 +109,12 @@ interface IGsmConverter { */ function REDEMPTION_CONTRACT() external view returns (address); + /** + * @notice Returns the address of the on-ramp contract that manages asset on-ramp + * @return The address of the on-ramp contract + */ + function ONRAMP_CONTRACT() external view returns (address); + /** * @notice Returns the current nonce (for EIP-712 signature methods) of an address * @param user The address of the user From 4e4055968e282267f76d49bcc9436c75cdc55e62 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 12:00:00 -0500 Subject: [PATCH 34/68] refactor: rename vars, update docs --- .../gsm/converter/GsmConverter.sol | 11 +++++----- .../converter/interfaces/IGsmConverter.sol | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 1c94956f..b3099bef 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -43,7 +43,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address public immutable REDEMPTION_CONTRACT; /// @inheritdoc IGsmConverter - address public immutable ONRAMP_CONTRACT; + address public immutable ISSUANCE_RECEIVER_CONTRACT; /// @inheritdoc IGsmConverter mapping(address => uint256) public nonces; @@ -51,7 +51,8 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { /** * @dev Constructor * @param gsm The address of the associated GSM contract - * @param redemptionContract The address of the redemption contract associated with the redemption/conversion + * @param redemptionContract The address of the redemption contract associated with the asset conversion + * @param issuanceReceiverContract The address of the contract receiving the payment associated with the asset conversion * @param redeemableAsset The address of the asset being redeemed * @param redeemedAsset The address of the asset being received from redemption */ @@ -59,20 +60,20 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address admin, address gsm, address redemptionContract, - address onrampContract, + address issuanceReceiverContract, address redeemableAsset, address redeemedAsset ) EIP712('GSMConverter', '1') { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(onrampContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(issuanceContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); GSM = gsm; REDEMPTION_CONTRACT = redemptionContract; - ONRAMP_CONTRACT = onrampContract; + ISSUANCE_RECEIVER_CONTRACT = issuanceReceiverContract; REDEEMABLE_ASSET = redeemableAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index e679ae2d..4d5e355b 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -22,6 +22,20 @@ interface IGsmConverter { uint256 ghoAmount ); + /** + * @dev Emitted when a user sells an asset (buying GHO) in the GSM after an issuance + * @param originator The address of the seller originating the request + * @param receiver The address of the receiver of GHO + * @param redeemedAssetAmount The amount of the redeemed asset converted + * @param ghoAmount The amount of GHO bought, inclusive of fee + */ + event SellAssetThroughIssuance( + address indexed originator, + address indexed receiver, + uint256 redeemedAssetAmount, + uint256 ghoAmount + ); + /** * @dev Emitted when tokens are rescued from the GSM converter * @param tokenRescued The address of the rescued token @@ -110,10 +124,10 @@ interface IGsmConverter { function REDEMPTION_CONTRACT() external view returns (address); /** - * @notice Returns the address of the on-ramp contract that manages asset on-ramp - * @return The address of the on-ramp contract + * @notice Returns the address of the issuance receiver contract that manages asset issuance + * @return The address of the issuance receiver contract */ - function ONRAMP_CONTRACT() external view returns (address); + function ISSUANCE_RECEIVER_CONTRACT() external view returns (address); /** * @notice Returns the current nonce (for EIP-712 signature methods) of an address From aa30e1b7a56e59021d69be507f767a89f989e80c Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 13:22:20 -0500 Subject: [PATCH 35/68] test: add docs for mock redemption --- src/test/mocks/MockRedemption.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/mocks/MockRedemption.sol b/src/test/mocks/MockRedemption.sol index 369ae404..7a9d2113 100644 --- a/src/test/mocks/MockRedemption.sol +++ b/src/test/mocks/MockRedemption.sol @@ -23,7 +23,8 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockRedemption + * @title MockRedemption Mock + * @dev Mock USDC-BUIDL off-ramp redemption contract. Prod version here invokes secondary contracts for liquidity: https://etherscan.io/address/0x31d3f59ad4aac0eee2247c65ebe8bf6e9e470a53#code * @dev Asset token is ERC20-compatible * @dev Liquidity token is ERC20-compatible */ From 3b96732dda1545867ec09318811efe9e3df028c7 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 13:42:22 -0500 Subject: [PATCH 36/68] feat: skeleton for sellAsset implementation --- .../gsm/converter/GsmConverter.sol | 34 ++++++++++-------- src/test/mocks/MockIssuanceReceiver.sol | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 src/test/mocks/MockIssuanceReceiver.sol diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index b3099bef..5033a0c4 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -10,6 +10,7 @@ import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; +import {MockIssuanceReceiver} from '../../../test/mocks/MockIssuanceReceiver.sol'; import 'forge-std/console2.sol'; @@ -192,20 +193,23 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { uint256 maxAmount, address receiver ) internal returns (uint256, uint256) { - // TODO: - // 2) implement sellAsset (sell USDC -> get GHO) - // - onramp USDC to BUIDL, get BUIDL - unknown how to onramp USDC to BUIDL currently - // - send BUIDL to GSM, get GHO from GSM - // - send GHO to user, safeTransfer - - ( - uint256 assetAmount, - uint256 ghoBought, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForSellAsset(maxAmount); - - // IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), assetAmount); - // IERC20(REDEEMED_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); + (uint256 redeemedAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = IGsm(GSM) + .getGhoAmountForSellAsset(maxAmount); + + // REDEEMED_ASSET, ie USDC. 1:1 ratio with REDEEMABLE_ASSET, ie BUIDL + IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); + IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, redeemedAssetAmount); + MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); // ie, USDC -> BUIDL + // reset approval after issuance + IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, 0); + + IERC20(REDEEMABLE_ASSET).approve(GSM, redeemedAssetAmount); + (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); + require(assetAmount == redeemedAssetAmount, 'INVALID_ASSET_SOLD'); + // reset approval after sellAsset + IERC20(REDEEMABLE_ASSET).approve(GSM, 0); + + emit SellAssetThroughIssuance(originator, receiver, redeemedAssetAmount, ghoBought); + return (assetAmount, ghoBought); } } diff --git a/src/test/mocks/MockIssuanceReceiver.sol b/src/test/mocks/MockIssuanceReceiver.sol new file mode 100644 index 00000000..e82452ba --- /dev/null +++ b/src/test/mocks/MockIssuanceReceiver.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.8.10; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockIssuanceReceiver + */ +contract MockIssuanceReceiver { + using SafeERC20 for IERC20; + + address public immutable asset; + address public immutable liquidity; + + /** + * @param _asset Address of asset token, ie BUIDL + * @param _liquidity Address of liquidity token, ie USDC + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @notice Issue the asset token in exchange for selling the liquidity token + */ + function issuance(uint256 amount) external { + IERC20(liquidity).safeTransferFrom(msg.sender, address(this), amount); + IERC20(asset).safeTransfer(msg.sender, amount); + } +} From d79ebde39d987e7d00060290aedad0d815a747c6 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 13:58:05 -0500 Subject: [PATCH 37/68] feat: add remappings for mocks --- remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/remappings.txt b/remappings.txt index e7ac529f..7c6ed2e7 100644 --- a/remappings.txt +++ b/remappings.txt @@ -15,3 +15,4 @@ aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-periphery/ erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests/ openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/ solidity-utils/=lib/solidity-utils/src/ +mocks/=src/test/mocks/ From 80a2c7237f05488394ed162267a588c61b5e57ed Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 13:58:33 -0500 Subject: [PATCH 38/68] test: resolve test compilations with additional constructor param --- .../gsm/converter/GsmConverter.sol | 26 +++++++++++++++---- src/test/TestGhoBase.t.sol | 4 +++ src/test/TestGsmConverter.t.sol | 23 ++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 5033a0c4..3f391bf7 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -10,7 +10,7 @@ import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; -import {MockIssuanceReceiver} from '../../../test/mocks/MockIssuanceReceiver.sol'; +import {MockIssuanceReceiver} from 'mocks/MockIssuanceReceiver.sol'; import 'forge-std/console2.sol'; @@ -68,7 +68,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(issuanceContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(issuanceReceiverContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); @@ -116,7 +116,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { /// @inheritdoc IGsmConverter function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256) { - require(minAmount > 0, 'INVALID_MIN_AMOUNT'); + require(maxAmount > 0, 'INVALID_MAX_AMOUNT'); return _sellAsset(msg.sender, maxAmount, receiver); } @@ -193,8 +193,11 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { uint256 maxAmount, address receiver ) internal returns (uint256, uint256) { - (uint256 redeemedAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = IGsm(GSM) - .getGhoAmountForSellAsset(maxAmount); + uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); + uint256 initialRedeemableAssetBalance = IERC20(REDEEMABLE_ASSET).balanceOf(address(this)); + uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); + + (uint256 redeemedAssetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); // REDEEMED_ASSET, ie USDC. 1:1 ratio with REDEEMABLE_ASSET, ie BUIDL IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); @@ -209,6 +212,19 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { // reset approval after sellAsset IERC20(REDEEMABLE_ASSET).approve(GSM, 0); + require( + IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, + 'INVALID_REMAINING_GHO_BALANCE' + ); + require( + IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == initialRedeemableAssetBalance, + 'INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE' + ); + require( + IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance, + 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' + ); + emit SellAssetThroughIssuance(originator, receiver, redeemedAssetAmount, ghoBought); return (assetAmount, ghoBought); } diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 0c66bbac..c893f314 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -34,6 +34,7 @@ import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; import {MockRedemptionFailedRedeemableAssetAmount} from './mocks/MockRedemptionFailedRedeemableAssetAmount.sol'; import {MockRedemptionFailedRedeemedAssetAmount} from './mocks/MockRedemptionFailedRedeemedAssetAmount.sol'; +import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -117,6 +118,7 @@ contract TestGhoBase is Test, Constants, Events { MockRedemption BUIDL_USDC_REDEMPTION; MockRedemptionFailedRedeemableAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT; MockRedemptionFailedRedeemedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT; + MockIssuanceReceiver BUIDL_USDC_ISSUANCE; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -354,11 +356,13 @@ contract TestGhoBase is Test, Constants, Events { address(BUIDL_TOKEN), address(USDC_TOKEN) ); + BUIDL_USDC_ISSUANCE = new MockIssuanceReceiver(address(BUIDL_TOKEN), address(USDC_TOKEN)); GSM_CONVERTER = new GsmConverter( address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 43686876..cb2071b7 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -17,6 +17,7 @@ contract TestGsmConverter is TestGhoBase { address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); @@ -27,6 +28,11 @@ contract TestGsmConverter is TestGhoBase { address(BUIDL_USDC_REDEMPTION), 'Unexpected redemption contract address' ); + assertEq( + gsmConverter.ISSUANCE_RECEIVER_CONTRACT(), + address(BUIDL_USDC_ISSUANCE), + 'Unexpected issuance receiver contract address' + ); assertEq( gsmConverter.REDEEMABLE_ASSET(), address(BUIDL_TOKEN), @@ -45,6 +51,7 @@ contract TestGsmConverter is TestGhoBase { address(0), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); @@ -54,6 +61,17 @@ contract TestGsmConverter is TestGhoBase { address(this), address(0), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(0), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); @@ -62,6 +80,7 @@ contract TestGsmConverter is TestGhoBase { new GsmConverter( address(this), address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), address(0), address(BUIDL_TOKEN), address(USDC_TOKEN) @@ -72,6 +91,7 @@ contract TestGsmConverter is TestGhoBase { address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), address(0), address(USDC_TOKEN) ); @@ -81,6 +101,7 @@ contract TestGsmConverter is TestGhoBase { address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(0) ); @@ -342,6 +363,7 @@ contract TestGsmConverter is TestGhoBase { address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); @@ -382,6 +404,7 @@ contract TestGsmConverter is TestGhoBase { address(this), address(GHO_BUIDL_GSM), address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT), + address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) ); From 2f94eaa3da16ec99c0046cb326c6d5880cad8869 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 14:43:56 -0500 Subject: [PATCH 39/68] test: happy path sellAsset emitting events --- .../gsm/converter/GsmConverter.sol | 5 ++-- src/test/TestGsmConverter.t.sol | 23 +++++++++++++++++++ src/test/helpers/Events.sol | 6 +++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 3f391bf7..30c34617 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -198,11 +198,10 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (uint256 redeemedAssetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); - - // REDEEMED_ASSET, ie USDC. 1:1 ratio with REDEEMABLE_ASSET, ie BUIDL IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, redeemedAssetAmount); - MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); // ie, USDC -> BUIDL + //TODO: replace with proper issuance implementation later + MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); // reset approval after issuance IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, 0); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index cb2071b7..93a33445 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1032,6 +1032,29 @@ contract TestGsmConverter is TestGhoBase { ); } + function testConverterSellAsset() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedRedeemableAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + ALICE + ); + vm.stopPrank(); + } + function _upgradeToGsmFailedGhoAmount() internal { address gsmFailed = address( new MockGsmFailedGhoAmount( diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index c24431e5..85235b8b 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -121,6 +121,12 @@ interface Events { uint256 redeemableAssetAmount, uint256 ghoAmount ); + event SellAssetThroughIssuance( + address indexed originator, + address indexed receiver, + uint256 redeemedAssetAmount, + uint256 ghoAmount + ); // FixedRateStrategyFactory event RateStrategyCreated(address indexed strategy, uint256 indexed rate); From 206160e6fab00a1ef663eae58f0869578b5a1d10 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 14:50:18 -0500 Subject: [PATCH 40/68] test: add happy path sellAsset assertions --- src/test/TestGsmConverter.t.sol | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 93a33445..1006753d 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1039,7 +1039,9 @@ contract TestGsmConverter is TestGhoBase { .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(FAUCET); + // Supply USDC to buyer USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + // Supply BUIDL to issuance contract BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); vm.stopPrank(); @@ -1053,6 +1055,31 @@ contract TestGsmConverter is TestGhoBase { ALICE ); vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (redeemable asset) balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); } function _upgradeToGsmFailedGhoAmount() internal { From acc78ca81891bd178500026a652c1b8073e945cd Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 16:48:46 -0500 Subject: [PATCH 41/68] test: invalid redemption refactor: rename buyAsset fail mock contracts, re-order methods test: separate check in buyAsset for redeemed asset --- .../gsm/converter/GsmConverter.sol | 13 +- src/test/TestGhoBase.t.sol | 17 +- src/test/TestGsmConverter.t.sol | 351 ++++++++++++++---- ...sol => MockGsmFailedBuyAssetGhoAmount.sol} | 4 +- ...kGsmFailedBuyAssetRemainingGhoBalance.sol} | 9 +- src/test/mocks/MockIssuanceReceiverFailed.sol | 37 ++ ...setAmount.sol => MockRedemptionFailed.sol} | 6 +- 7 files changed, 341 insertions(+), 96 deletions(-) rename src/test/mocks/{MockGsmFailedGhoAmount.sol => MockGsmFailedBuyAssetGhoAmount.sol} (99%) rename src/test/mocks/{MockGsmFailedRemainingGhoBalance.sol => MockGsmFailedBuyAssetRemainingGhoBalance.sol} (99%) create mode 100644 src/test/mocks/MockIssuanceReceiverFailed.sol rename src/test/mocks/{MockRedemptionFailedRedeemedAssetAmount.sol => MockRedemptionFailed.sol} (90%) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 30c34617..51c226dc 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -159,6 +159,11 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); + require( + IERC20(REDEEMED_ASSET).balanceOf(address(this)) == + initialRedeemedAssetBalance + redeemableAssetAmount, + 'INVALID_REDEMPTION' + ); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), 0); // redeemableAssetAmount matches redeemedAssetAmount because Redemption exchanges in 1:1 ratio IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); @@ -171,10 +176,6 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == initialRedeemableAssetBalance, 'INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE' ); - require( - IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance, - 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' - ); emit BuyAssetThroughRedemption(originator, receiver, redeemableAssetAmount, ghoSold); return (redeemableAssetAmount, ghoSold); @@ -202,6 +203,10 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, redeemedAssetAmount); //TODO: replace with proper issuance implementation later MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); + require( + IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == redeemedAssetAmount, + 'INVALID_ISSUANCE' + ); // reset approval after issuance IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, 0); diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index c893f314..d0965cb0 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -22,8 +22,8 @@ import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; -import {MockGsmFailedGhoAmount} from './mocks/MockGsmFailedGhoAmount.sol'; -import {MockGsmFailedRemainingGhoBalance} from './mocks/MockGsmFailedRemainingGhoBalance.sol'; +import {MockGsmFailedBuyAssetGhoAmount} from './mocks/MockGsmFailedBuyAssetGhoAmount.sol'; +import {MockGsmFailedBuyAssetRemainingGhoBalance} from './mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockAddressesProvider} from './mocks/MockAddressesProvider.sol'; import {MockERC4626} from './mocks/MockERC4626.sol'; @@ -33,8 +33,9 @@ import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/T import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; import {MockRedemptionFailedRedeemableAssetAmount} from './mocks/MockRedemptionFailedRedeemableAssetAmount.sol'; -import {MockRedemptionFailedRedeemedAssetAmount} from './mocks/MockRedemptionFailedRedeemedAssetAmount.sol'; +import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; +import {MockIssuanceReceiverFailed} from './mocks/MockIssuanceReceiverFailed.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -117,8 +118,9 @@ contract TestGhoBase is Test, Constants, Events { MockConfigurator CONFIGURATOR; MockRedemption BUIDL_USDC_REDEMPTION; MockRedemptionFailedRedeemableAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT; - MockRedemptionFailedRedeemedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT; + MockRedemptionFailed BUIDL_USDC_REDEMPTION_FAILED; MockIssuanceReceiver BUIDL_USDC_ISSUANCE; + MockIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -352,12 +354,15 @@ contract TestGhoBase is Test, Constants, Events { address(BUIDL_TOKEN), address(USDC_TOKEN) ); - BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT = new MockRedemptionFailedRedeemedAssetAmount( + BUIDL_USDC_REDEMPTION_FAILED = new MockRedemptionFailed( address(BUIDL_TOKEN), address(USDC_TOKEN) ); BUIDL_USDC_ISSUANCE = new MockIssuanceReceiver(address(BUIDL_TOKEN), address(USDC_TOKEN)); - + BUIDL_USDC_ISSUANCE_FAILED = new MockIssuanceReceiverFailed( + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); GSM_CONVERTER = new GsmConverter( address(this), address(GHO_BUIDL_GSM), diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 1006753d..d7691ca7 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -107,6 +107,206 @@ contract TestGsmConverter is TestGhoBase { ); } + function testSellAsset() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedRedeemableAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + ALICE + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (redeemable asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedRedeemableAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + + function testSellAssetSendToOther() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, BOB, expectedRedeemableAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + BOB + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(BOB), 0, 'Unexpected receiver final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(BOB), ghoBought, 'Unexpected receiver final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(BOB), + 0, + 'Unexpected receiver final BUIDL (redeemable asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (redeemable asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedRedeemableAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + + function testRevertSellAssetZeroAmount() public { + vm.prank(ALICE); + vm.expectRevert('INVALID_MAX_AMOUNT'); + GSM_CONVERTER.sellAsset(0, ALICE); + } + + function testRevertSellAssetNoAsset() public { + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + vm.expectRevert('ERC20: transfer amount exceeds balance'); + GSM_CONVERTER.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + } + + function testRevertSellAssetNoAllowanceRedeemedAsset() public { + vm.prank(FAUCET); + USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + + vm.prank(ALICE); + vm.expectRevert('ERC20: transfer amount exceeds allowance'); + GSM_CONVERTER.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + } + + function testRevertSellAssetInvalidIssuance() public { + GsmConverter gsmConverter = new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE_FAILED), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + vm.startPrank(FAUCET); + USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(gsmConverter), DEFAULT_GSM_BUIDL_AMOUNT); + vm.expectRevert('INVALID_ISSUANCE'); + gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + } + function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); @@ -297,7 +497,7 @@ contract TestGsmConverter is TestGhoBase { } function testRevertBuyAssetInvalidGhoSold() public { - _upgradeToGsmFailedGhoAmount(); + _upgradeToGsmFailedBuyAssetGhoAmount(); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( @@ -328,7 +528,7 @@ contract TestGsmConverter is TestGhoBase { } function testRevertBuyAssetInvalidRemainingGhoBalance() public { - _upgradeToGsmFailedRemainingGhoBalance(); + _upgradeToGsmFailedBuyAssetRemainingGhoBalance(); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( @@ -399,11 +599,11 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } - function testRevertBuyAssetInvalidRemainingRedeemedAssetBalance() public { + function testRevertBuyAssetInvalidRedemption() public { GsmConverter gsmConverter = new GsmConverter( address(this), address(GHO_BUIDL_GSM), - address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT), + address(BUIDL_USDC_REDEMPTION_FAILED), address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) @@ -426,7 +626,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(FAUCET); uint256 bufferForAdditionalTransfer = 1000; USDC_TOKEN.mint( - address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMED_ASSET_AMOUNT), + address(BUIDL_USDC_REDEMPTION_FAILED), DEFAULT_GSM_BUIDL_AMOUNT + bufferForAdditionalTransfer ); @@ -435,12 +635,55 @@ contract TestGsmConverter is TestGhoBase { vm.startPrank(BOB); GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); - // Buy assets via Redemption of USDC - vm.expectRevert('INVALID_REMAINING_REDEEMED_ASSET_BALANCE'); + // Invalid redemption of USDC + vm.expectRevert('INVALID_REDEMPTION'); gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); vm.stopPrank(); } + /// TODO: @dev Assuming an attacker donates BUIDL token to the converter + // function testRevertBuyAssetInvalidRedemptionNonZeroBalance() public { + // GsmConverter gsmConverter = new GsmConverter( + // address(this), + // address(GHO_BUIDL_GSM), + // address(BUIDL_USDC_REDEMPTION_FAILED), + // address(BUIDL_USDC_ISSUANCE), + // address(BUIDL_TOKEN), + // address(USDC_TOKEN) + // ); + + // uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + // (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( + // DEFAULT_GSM_BUIDL_AMOUNT + // ); + + // // Supply BUIDL assets to the BUIDL GSM first + // vm.prank(FAUCET); + // BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + // vm.startPrank(ALICE); + // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + // GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + // vm.stopPrank(); + + // // Supply USDC to the Redemption contract + // vm.prank(FAUCET); + // uint256 bufferForAdditionalTransfer = 1000; + // USDC_TOKEN.mint( + // address(BUIDL_USDC_REDEMPTION_FAILED), + // DEFAULT_GSM_BUIDL_AMOUNT + bufferForAdditionalTransfer + // ); + + // // Supply assets to another user + // ghoFaucet(BOB, expectedGhoSold + buyFee); + // vm.startPrank(BOB); + // GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); + + // // Invalid redemption of USDC + // vm.expectRevert('INVALID_REDEMPTION'); + // gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); + // vm.stopPrank(); + // } + function testBuyAssetWithSig() public { (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); @@ -668,22 +911,6 @@ contract TestGsmConverter is TestGhoBase { ); } - function _getBuySellFees( - uint256 amount - ) - private - returns ( - uint256 expectedRedeemedAssetAmount, - uint256 expectedGhoSold, - uint256 buyFee, - uint256 sell - ) - { - (expectedRedeemedAssetAmount, expectedGhoSold, , buyFee) = GHO_BUIDL_GSM - .getGhoAmountForBuyAsset(amount); - (, , , sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(amount); - } - function testFuzzMinAmountBuyAssetWithSig(uint minAssetAmount) public { minAssetAmount = bound(minAssetAmount, 1, DEFAULT_GSM_BUIDL_AMOUNT * 1000); @@ -1032,66 +1259,16 @@ contract TestGsmConverter is TestGhoBase { ); } - function testConverterSellAsset() public { - uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM - .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); - - vm.startPrank(FAUCET); - // Supply USDC to buyer - USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); - // Supply BUIDL to issuance contract - BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); - vm.stopPrank(); - - vm.startPrank(ALICE); - USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); - - vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedRedeemableAssetAmount, expectedGhoBought); - (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( - DEFAULT_GSM_BUIDL_AMOUNT, - ALICE - ); - vm.stopPrank(); - - assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); - assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); - assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); - assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); - assertEq( - BUIDL_TOKEN.balanceOf(ALICE), - 0, - 'Unexpected seller final BUIDL (redeemable asset) balance' - ); - assertEq( - USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - 0, - 'Unexpected GSM_CONVERTER final USDC balance' - ); - assertEq( - BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), - 0, - 'Unexpected GSM_CONVERTER final BUIDL balance' - ); - assertEq( - GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), - 0, - 'Unexpected GSM_CONVERTER final GHO balance' - ); - } - - function _upgradeToGsmFailedGhoAmount() internal { + function _upgradeToGsmFailedBuyAssetGhoAmount() internal { address gsmFailed = address( - new MockGsmFailedGhoAmount( + new MockGsmFailedBuyAssetGhoAmount( address(GHO_TOKEN), address(BUIDL_TOKEN), address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) ) ); bytes memory data = abi.encodeWithSelector( - MockGsmFailedGhoAmount.initialize.selector, + MockGsmFailedBuyAssetGhoAmount.initialize.selector, address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE @@ -1101,16 +1278,16 @@ contract TestGsmConverter is TestGhoBase { AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); } - function _upgradeToGsmFailedRemainingGhoBalance() internal { + function _upgradeToGsmFailedBuyAssetRemainingGhoBalance() internal { address gsmFailed = address( - new MockGsmFailedRemainingGhoBalance( + new MockGsmFailedBuyAssetRemainingGhoBalance( address(GHO_TOKEN), address(BUIDL_TOKEN), address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) ) ); bytes memory data = abi.encodeWithSelector( - MockGsmFailedRemainingGhoBalance.initialize.selector, + MockGsmFailedBuyAssetRemainingGhoBalance.initialize.selector, address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE @@ -1119,4 +1296,20 @@ contract TestGsmConverter is TestGhoBase { vm.prank(SHORT_EXECUTOR); AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); } + + function _getBuySellFees( + uint256 amount + ) + internal + returns ( + uint256 expectedRedeemedAssetAmount, + uint256 expectedGhoSold, + uint256 buyFee, + uint256 sell + ) + { + (expectedRedeemedAssetAmount, expectedGhoSold, , buyFee) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(amount); + (, , , sell) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(amount); + } } diff --git a/src/test/mocks/MockGsmFailedGhoAmount.sol b/src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol similarity index 99% rename from src/test/mocks/MockGsmFailedGhoAmount.sol rename to src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol index 7d922666..2b6593e5 100644 --- a/src/test/mocks/MockGsmFailedGhoAmount.sol +++ b/src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol @@ -15,11 +15,11 @@ import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/inte import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; /** - * @title MockGsmFailedGhoAmount + * @title MockGsmFailedBuyAssetGhoAmount * @author Aave * @notice GSM that fails calculation for GHO amount in getGhoAmountForBuyAsset */ -contract MockGsmFailedGhoAmount is AccessControl, VersionedInitializable, EIP712, IGsm { +contract MockGsmFailedBuyAssetGhoAmount is AccessControl, VersionedInitializable, EIP712, IGsm { using GPv2SafeERC20 for IERC20; using SafeCast for uint256; diff --git a/src/test/mocks/MockGsmFailedRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol similarity index 99% rename from src/test/mocks/MockGsmFailedRemainingGhoBalance.sol rename to src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol index 64eaa185..087e1cae 100644 --- a/src/test/mocks/MockGsmFailedRemainingGhoBalance.sol +++ b/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol @@ -15,11 +15,16 @@ import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/inte import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; /** - * @title MockGsmFailedRemainingGhoBalance + * @title MockGsmFailedBuyAssetRemainingGhoBalance * @author Aave * @notice GSM that transfers incorrect amount of GHO during buyAsset */ -contract MockGsmFailedRemainingGhoBalance is AccessControl, VersionedInitializable, EIP712, IGsm { +contract MockGsmFailedBuyAssetRemainingGhoBalance is + AccessControl, + VersionedInitializable, + EIP712, + IGsm +{ using GPv2SafeERC20 for IERC20; using SafeCast for uint256; diff --git a/src/test/mocks/MockIssuanceReceiverFailed.sol b/src/test/mocks/MockIssuanceReceiverFailed.sol new file mode 100644 index 00000000..d85376fe --- /dev/null +++ b/src/test/mocks/MockIssuanceReceiverFailed.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.10; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockIssuanceReceiverFailed + */ +contract MockIssuanceReceiverFailed { + using SafeERC20 for IERC20; + + address public immutable asset; + address public immutable liquidity; + + /** + * @param _asset Address of asset token, ie BUIDL + * @param _liquidity Address of liquidity token, ie USDC + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @notice Issue the asset token in exchange for selling the liquidity token + */ + function issuance(uint256 amount) external { + IERC20(liquidity).safeTransferFrom(msg.sender, address(this), amount); + // TRIGGER ERROR: no asset issued to msg.sender + // IERC20(asset).safeTransfer(msg.sender, amount); + } +} diff --git a/src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol b/src/test/mocks/MockRedemptionFailed.sol similarity index 90% rename from src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol rename to src/test/mocks/MockRedemptionFailed.sol index 77983783..b0f32bf8 100644 --- a/src/test/mocks/MockRedemptionFailedRedeemedAssetAmount.sol +++ b/src/test/mocks/MockRedemptionFailed.sol @@ -23,11 +23,11 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockRedemptionFailedRedeemedAssetAmount + * @title MockRedemptionFailed * @dev Asset token is ERC20-compatible * @dev Liquidity token is ERC20-compatible */ -contract MockRedemptionFailedRedeemedAssetAmount is IRedemption { +contract MockRedemptionFailed is IRedemption { using SafeERC20 for IERC20; /** @@ -60,6 +60,6 @@ contract MockRedemptionFailedRedeemedAssetAmount is IRedemption { function redeem(uint256 amount) external { // mock outcome from Redemption IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); - IERC20(liquidity).safeTransfer(msg.sender, amount + 1); + IERC20(liquidity).safeTransfer(msg.sender, amount - 1); } } From 59d72913d1f67b6a8b0df8f7a8a3af718458b9b5 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 17:42:24 -0500 Subject: [PATCH 42/68] test: fail cases for sellAsset with invalid asset amounts, add mocks feat: skeleton for sellAssetWithSig refactor: rename mock contract --- .../gsm/converter/GsmConverter.sol | 48 +- .../converter/interfaces/IGsmConverter.sol | 25 + src/test/TestGhoBase.t.sol | 9 +- src/test/TestGsmConverter.t.sol | 62 +- ... MockGsmFailedGetGhoAmountForBuyAsset.sol} | 9 +- ...kGsmFailedSellAssetRemainingGhoBalance.sol | 577 ++++++++++++++++++ ...uanceReceiverFailedInvalidUSDCAccepted.sol | 38 ++ 7 files changed, 752 insertions(+), 16 deletions(-) rename src/test/mocks/{MockGsmFailedBuyAssetGhoAmount.sol => MockGsmFailedGetGhoAmountForBuyAsset.sol} (99%) create mode 100644 src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol create mode 100644 src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 51c226dc..883c0f2d 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -28,6 +28,12 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' ); + /// @inheritdoc IGsmConverter + bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + /// @inheritdoc IGsmConverter address public immutable GHO_TOKEN; @@ -82,6 +88,13 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { transferOwnership(admin); } + /// @inheritdoc IGsmConverter + function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256) { + require(maxAmount > 0, 'INVALID_MAX_AMOUNT'); + + return _sellAsset(msg.sender, maxAmount, receiver); + } + /// @inheritdoc IGsmConverter function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256) { require(minAmount > 0, 'INVALID_MIN_AMOUNT'); @@ -115,10 +128,28 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { } /// @inheritdoc IGsmConverter - function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256) { - require(maxAmount > 0, 'INVALID_MAX_AMOUNT'); + function sellAssetWithSig( + address originator, + uint256 maxAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); - return _sellAsset(msg.sender, maxAmount, receiver); + return _sellAsset(originator, maxAmount, receiver); } /// @inheritdoc IGsmConverter @@ -159,13 +190,14 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); + // redeemedAssetAmount matches redeemableAssetAmount because Redemption exchanges in 1:1 ratio require( IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance + redeemableAssetAmount, 'INVALID_REDEMPTION' ); IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), 0); - // redeemableAssetAmount matches redeemedAssetAmount because Redemption exchanges in 1:1 ratio + IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); require( @@ -204,7 +236,8 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { //TODO: replace with proper issuance implementation later MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); require( - IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == redeemedAssetAmount, + IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == + initialRedeemedAssetBalance + redeemedAssetAmount, 'INVALID_ISSUANCE' ); // reset approval after issuance @@ -212,7 +245,6 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMABLE_ASSET).approve(GSM, redeemedAssetAmount); (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); - require(assetAmount == redeemedAssetAmount, 'INVALID_ASSET_SOLD'); // reset approval after sellAsset IERC20(REDEEMABLE_ASSET).approve(GSM, 0); @@ -220,10 +252,6 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, 'INVALID_REMAINING_GHO_BALANCE' ); - require( - IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == initialRedeemableAssetBalance, - 'INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE' - ); require( IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance, 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 4d5e355b..e867d4ad 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -76,6 +76,25 @@ interface IGsmConverter { bytes calldata signature ) external returns (uint256, uint256); + /** + * @notice Sells the GSM underlying asset in exchange for buying GHO after asset issuance, using an EIP-712 signature + * @dev Use `getAssetAmountForSellAsset` function to calculate the amount based on the GHO amount to buy + * @param originator The signer of the request + * @param maxAmount The maximum amount of the underlying asset to sell + * @param receiver Recipient address of the GHO being purchased + * @param deadline Signature expiration deadline + * @param signature Signature data + * @return The amount of underlying asset sold + * @return The amount of GHO bought by the user + */ + function sellAssetWithSig( + address originator, + uint256 maxAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external returns (uint256, uint256); + /** * @notice Sells the GSM underlying asset in exchange for buying GHO, after asset conversion * @param maxAmount The maximum amount of the underlying asset to sell @@ -147,4 +166,10 @@ interface IGsmConverter { * @return The bytes32 signature typehash for buyAssetWithSig */ function BUY_ASSET_WITH_SIG_TYPEHASH() external pure returns (bytes32); + + /** + * @notice Returns the EIP-712 signature typehash for sellAssetWithSig + * @return The bytes32 signature typehash for sellAssetWithSig + */ + function SELL_ASSET_WITH_SIG_TYPEHASH() external pure returns (bytes32); } diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index d0965cb0..e8094666 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -22,8 +22,9 @@ import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; -import {MockGsmFailedBuyAssetGhoAmount} from './mocks/MockGsmFailedBuyAssetGhoAmount.sol'; +import {MockGsmFailedGetGhoAmountForBuyAsset} from './mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol'; import {MockGsmFailedBuyAssetRemainingGhoBalance} from './mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol'; +import {MockGsmFailedSellAssetRemainingGhoBalance} from './mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockAddressesProvider} from './mocks/MockAddressesProvider.sol'; import {MockERC4626} from './mocks/MockERC4626.sol'; @@ -36,6 +37,7 @@ import {MockRedemptionFailedRedeemableAssetAmount} from './mocks/MockRedemptionF import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; import {MockIssuanceReceiverFailed} from './mocks/MockIssuanceReceiverFailed.sol'; +import {MockIssuanceReceiverFailedInvalidUSDCAccepted} from './mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -121,6 +123,7 @@ contract TestGhoBase is Test, Constants, Events { MockRedemptionFailed BUIDL_USDC_REDEMPTION_FAILED; MockIssuanceReceiver BUIDL_USDC_ISSUANCE; MockIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; + MockIssuanceReceiverFailedInvalidUSDCAccepted BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -363,6 +366,10 @@ contract TestGhoBase is Test, Constants, Events { address(BUIDL_TOKEN), address(USDC_TOKEN) ); + BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC = new MockIssuanceReceiverFailedInvalidUSDCAccepted( + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); GSM_CONVERTER = new GsmConverter( address(this), address(GHO_BUIDL_GSM), diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index d7691ca7..1ce9e966 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -297,16 +297,53 @@ contract TestGsmConverter is TestGhoBase { address(USDC_TOKEN) ); + vm.prank(FAUCET); + USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(gsmConverter), DEFAULT_GSM_BUIDL_AMOUNT); + vm.expectRevert('INVALID_ISSUANCE'); + gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + } + + function testRevertSellAssetInvalidRemainingGhoBalance() public { + _upgradeToGsmFailedSellAssetRemainingGhoBalance(); + vm.startPrank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), DEFAULT_GSM_BUIDL_AMOUNT); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_BUIDL_AMOUNT); + vm.expectRevert('INVALID_REMAINING_GHO_BALANCE'); + GSM_CONVERTER.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + } + + function testRevertSellAssetInvalidRedeemedAssetBalance() public { + GsmConverter gsmConverter = new GsmConverter( + address(this), + address(GHO_BUIDL_GSM), + address(BUIDL_USDC_REDEMPTION), + address(BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC), + address(BUIDL_TOKEN), + address(USDC_TOKEN) + ); + + vm.startPrank(FAUCET); + USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC), DEFAULT_GSM_BUIDL_AMOUNT); vm.stopPrank(); vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsmConverter), DEFAULT_GSM_BUIDL_AMOUNT); - vm.expectRevert('INVALID_ISSUANCE'); + vm.expectRevert('INVALID_REMAINING_REDEEMED_ASSET_BALANCE'); gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); } + // TODO: Add test for sellAssetWithSig + // TODO: test for buyAsset failed redeemable asset amount? + function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); @@ -1261,14 +1298,14 @@ contract TestGsmConverter is TestGhoBase { function _upgradeToGsmFailedBuyAssetGhoAmount() internal { address gsmFailed = address( - new MockGsmFailedBuyAssetGhoAmount( + new MockGsmFailedGetGhoAmountForBuyAsset( address(GHO_TOKEN), address(BUIDL_TOKEN), address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) ) ); bytes memory data = abi.encodeWithSelector( - MockGsmFailedBuyAssetGhoAmount.initialize.selector, + MockGsmFailedGetGhoAmountForBuyAsset.initialize.selector, address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE @@ -1297,6 +1334,25 @@ contract TestGsmConverter is TestGhoBase { AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); } + function _upgradeToGsmFailedSellAssetRemainingGhoBalance() internal { + address gsmFailed = address( + new MockGsmFailedSellAssetRemainingGhoBalance( + address(GHO_TOKEN), + address(BUIDL_TOKEN), + address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) + ) + ); + bytes memory data = abi.encodeWithSelector( + MockGsmFailedSellAssetRemainingGhoBalance.initialize.selector, + address(this), + TREASURY, + DEFAULT_GSM_USDC_EXPOSURE + ); + + vm.prank(SHORT_EXECUTOR); + AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); + } + function _getBuySellFees( uint256 amount ) diff --git a/src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol b/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol similarity index 99% rename from src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol rename to src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol index 2b6593e5..b61b1da7 100644 --- a/src/test/mocks/MockGsmFailedBuyAssetGhoAmount.sol +++ b/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol @@ -15,11 +15,16 @@ import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/inte import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; /** - * @title MockGsmFailedBuyAssetGhoAmount + * @title MockGsmFailedGetGhoAmountForBuyAsset * @author Aave * @notice GSM that fails calculation for GHO amount in getGhoAmountForBuyAsset */ -contract MockGsmFailedBuyAssetGhoAmount is AccessControl, VersionedInitializable, EIP712, IGsm { +contract MockGsmFailedGetGhoAmountForBuyAsset is + AccessControl, + VersionedInitializable, + EIP712, + IGsm +{ using GPv2SafeERC20 for IERC20; using SafeCast for uint256; diff --git a/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol new file mode 100644 index 00000000..7093746b --- /dev/null +++ b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; +import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; +import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; +import {IGhoFacilitator} from '../../contracts/gho/interfaces/IGhoFacilitator.sol'; +import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; +import {IGsmPriceStrategy} from '../../contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; +import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; + +/** + * @title MockGsmFailedSellAssetRemainingGhoBalance + * @author Aave + * @notice GSM that transfers incorrect amount of GHO during sellAsset + */ +contract MockGsmFailedSellAssetRemainingGhoBalance is + AccessControl, + VersionedInitializable, + EIP712, + IGsm +{ + using GPv2SafeERC20 for IERC20; + using SafeCast for uint256; + + /// @inheritdoc IGsm + bytes32 public constant CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); + + /// @inheritdoc IGsm + bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + + /// @inheritdoc IGsm + address public immutable GHO_TOKEN; + + /// @inheritdoc IGsm + address public immutable UNDERLYING_ASSET; + + /// @inheritdoc IGsm + address public immutable PRICE_STRATEGY; + + /// @inheritdoc IGsm + mapping(address => uint256) public nonces; + + address internal _ghoTreasury; + address internal _feeStrategy; + bool internal _isFrozen; + bool internal _isSeized; + uint128 internal _exposureCap; + uint128 internal _currentExposure; + uint128 internal _accruedFees; + + /** + * @dev Require GSM to not be frozen for functions marked by this modifier + */ + modifier notFrozen() { + require(!_isFrozen, 'GSM_FROZEN'); + _; + } + + /** + * @dev Require GSM to not be seized for functions marked by this modifier + */ + modifier notSeized() { + require(!_isSeized, 'GSM_SEIZED'); + _; + } + + /** + * @dev Constructor + * @param ghoToken The address of the GHO token contract + * @param underlyingAsset The address of the collateral asset + * @param priceStrategy The address of the price strategy + */ + constructor(address ghoToken, address underlyingAsset, address priceStrategy) EIP712('GSM', '1') { + require(ghoToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require( + IGsmPriceStrategy(priceStrategy).UNDERLYING_ASSET() == underlyingAsset, + 'INVALID_PRICE_STRATEGY' + ); + GHO_TOKEN = ghoToken; + UNDERLYING_ASSET = underlyingAsset; + PRICE_STRATEGY = priceStrategy; + } + + /** + * @notice GSM initializer + * @param admin The address of the default admin role + * @param ghoTreasury The address of the GHO treasury + * @param exposureCap Maximum amount of user-supplied underlying asset in GSM + */ + function initialize( + address admin, + address ghoTreasury, + uint128 exposureCap + ) external initializer { + require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(CONFIGURATOR_ROLE, admin); + _updateGhoTreasury(ghoTreasury); + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGsm + function buyAsset( + uint256 minAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _buyAsset(msg.sender, minAmount, receiver); + } + + /// @inheritdoc IGsm + function buyAssetWithSig( + address originator, + uint256 minAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _buyAsset(originator, minAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAsset( + uint256 maxAmount, + address receiver + ) external notFrozen notSeized returns (uint256, uint256) { + return _sellAsset(msg.sender, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function sellAssetWithSig( + address originator, + uint256 maxAmount, + address receiver, + uint256 deadline, + bytes calldata signature + ) external notFrozen notSeized returns (uint256, uint256) { + require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + _domainSeparatorV4(), + SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) + ) + ); + require( + SignatureChecker.isValidSignatureNow(originator, digest, signature), + 'SIGNATURE_INVALID' + ); + + return _sellAsset(originator, maxAmount, receiver); + } + + /// @inheritdoc IGsm + function rescueTokens( + address token, + address to, + uint256 amount + ) external onlyRole(TOKEN_RESCUER_ROLE) { + require(amount > 0, 'INVALID_AMOUNT'); + if (token == GHO_TOKEN) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; + require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); + } + if (token == UNDERLYING_ASSET) { + uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; + require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); + } + IERC20(token).safeTransfer(to, amount); + emit TokensRescued(token, to, amount); + } + + /// @inheritdoc IGsm + function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { + if (enable) { + require(!_isFrozen, 'GSM_ALREADY_FROZEN'); + } else { + require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); + } + _isFrozen = enable; + emit SwapFreeze(msg.sender, enable); + } + + /// @inheritdoc IGsm + function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + _isSeized = true; + _currentExposure = 0; + _updateExposureCap(0); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); + if (underlyingBalance > 0) { + IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); + } + + emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); + return underlyingBalance; + } + + /// @inheritdoc IGsm + function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { + require(_isSeized, 'GSM_NOT_SEIZED'); + require(amount > 0, 'INVALID_AMOUNT'); + + (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); + if (amount > ghoMinted) { + amount = ghoMinted; + } + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); + IGhoToken(GHO_TOKEN).burn(amount); + + emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); + return amount; + } + + /// @inheritdoc IGsm + function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { + _updateFeeStrategy(feeStrategy); + } + + /// @inheritdoc IGsm + function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { + _updateExposureCap(exposureCap); + } + + /// @inheritdoc IGhoFacilitator + function distributeFeesToTreasury() public virtual override { + uint256 accruedFees = _accruedFees; + if (accruedFees > 0) { + _accruedFees = 0; + IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); + emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); + } + } + + /// @inheritdoc IGhoFacilitator + function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { + _updateGhoTreasury(newGhoTreasury); + } + + /// @inheritdoc IGsm + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IGsm + function getGhoAmountForBuyAsset( + uint256 minAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAssetAmount); + return (assetAmount, ghoSold, grossAmount, fee); + } + + /// @inheritdoc IGsm + function getGhoAmountForSellAsset( + uint256 maxAssetAmount + ) external view returns (uint256, uint256, uint256, uint256) { + return _calculateGhoAmountForSellAsset(maxAssetAmount); + } + + /// @inheritdoc IGsm + function getAssetAmountForBuyAsset( + uint256 maxGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) + : maxGhoAmount; + // round down so maxGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + true + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAssetAmountForSellAsset( + uint256 minGhoAmount + ) external view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + uint256 grossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(minGhoAmount) + : minGhoAmount; + // round up so minGhoAmount is guaranteed + uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, true); + uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( + assetAmount, + false + ); + uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(finalGrossAmount) : 0; + return (assetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /// @inheritdoc IGsm + function getAvailableUnderlyingExposure() external view returns (uint256) { + return _exposureCap > _currentExposure ? _exposureCap - _currentExposure : 0; + } + + /// @inheritdoc IGsm + function getExposureCap() external view returns (uint128) { + return _exposureCap; + } + + /// @inheritdoc IGsm + function getAvailableLiquidity() external view returns (uint256) { + return _currentExposure; + } + + /// @inheritdoc IGsm + function getFeeStrategy() external view returns (address) { + return _feeStrategy; + } + + /// @inheritdoc IGsm + function getAccruedFees() external view returns (uint256) { + return _accruedFees; + } + + /// @inheritdoc IGsm + function getIsFrozen() external view returns (bool) { + return _isFrozen; + } + + /// @inheritdoc IGsm + function getIsSeized() external view returns (bool) { + return _isSeized; + } + + /// @inheritdoc IGsm + function canSwap() external view returns (bool) { + return !_isFrozen && !_isSeized; + } + + /// @inheritdoc IGhoFacilitator + function getGhoTreasury() external view override returns (address) { + return _ghoTreasury; + } + + /// @inheritdoc IGsm + function GSM_REVISION() public pure virtual override returns (uint256) { + return 2; + } + + /** + * @dev Buys an underlying asset with GHO + * @param originator The originator of the request + * @param minAmount The minimum amount of the underlying asset desired for purchase + * @param receiver The recipient address of the underlying asset being purchased + * @return The amount of underlying asset bought + * @return The amount of GHO sold by the user + */ + function _buyAsset( + address originator, + uint256 minAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoSold, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForBuyAsset(minAmount); + + _beforeBuyAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); + + _currentExposure -= uint128(assetAmount); + _accruedFees += fee.toUint128(); + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); + IGhoToken(GHO_TOKEN).burn(grossAmount); + IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); + + emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); + return (assetAmount, ghoSold); + } + + /** + * @dev Hook that is called before `buyAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired for purchase + * @param receiver Recipient address of the underlying asset being purchased + */ + function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} + + /** + * @dev Sells an underlying asset for GHO + * @param originator The originator of the request + * @param maxAmount The maximum amount of the underlying asset desired to sell + * @param receiver The recipient address of the GHO being purchased + * @return The amount of underlying asset sold + * @return The amount of GHO bought by the user + */ + function _sellAsset( + address originator, + uint256 maxAmount, + address receiver + ) internal returns (uint256, uint256) { + ( + uint256 assetAmount, + uint256 ghoBought, + uint256 grossAmount, + uint256 fee + ) = _calculateGhoAmountForSellAsset(maxAmount); + + _beforeSellAsset(originator, assetAmount, receiver); + + require(assetAmount > 0, 'INVALID_AMOUNT'); + require(_currentExposure + assetAmount <= _exposureCap, 'EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); + + _currentExposure += uint128(assetAmount); + _accruedFees += fee.toUint128(); + IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); + + IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); + IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); + // TRIGGER ERROR: invalid transfer of GHO amount to GSM Converter (msg.sender) + IGhoToken(GHO_TOKEN).transfer(msg.sender, 1); + + emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); + return (assetAmount, ghoBought); + } + + /** + * @dev Hook that is called before `sellAsset`. + * @dev This can be used to add custom logic + * @param originator Originator of the request + * @param amount The amount of the underlying asset desired to sell + * @param receiver Recipient address of the GHO being purchased + */ + function _beforeSellAsset( + address originator, + uint256 amount, + address receiver + ) internal virtual {} + + /** + * @dev Returns the amount of GHO sold in exchange of buying underlying asset + * @param assetAmount The amount of underlying asset to buy + * @return The exact amount of asset the user purchases + * @return The total amount of GHO the user sells (gross amount in GHO plus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied on top of gross amount of GHO + */ + function _calculateGhoAmountForBuyAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the highest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; + uint256 ghoSold = grossAmount + fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) + : ghoSold; + // pick the lowest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + false + ); + uint256 finalFee = ghoSold - finalGrossAmount; + return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset + * @param assetAmount The amount of underlying asset to sell + * @return The exact amount of asset the user sells + * @return The total amount of GHO the user buys (gross amount in GHO minus fee) + * @return The gross amount of GHO + * @return The fee amount in GHO, applied to the gross amount of GHO + */ + function _calculateGhoAmountForSellAsset( + uint256 assetAmount + ) internal view returns (uint256, uint256, uint256, uint256) { + bool withFee = _feeStrategy != address(0); + // pick the lowest GHO amount possible for given asset amount + uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, false); + uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(grossAmount) : 0; + uint256 ghoBought = grossAmount - fee; + uint256 finalGrossAmount = withFee + ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(ghoBought) + : ghoBought; + // pick the highest asset amount possible for given GHO amount + uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( + finalGrossAmount, + true + ); + uint256 finalFee = finalGrossAmount - ghoBought; + return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); + } + + /** + * @dev Updates Fee Strategy + * @param feeStrategy The address of the new Fee Strategy + */ + function _updateFeeStrategy(address feeStrategy) internal { + address oldFeeStrategy = _feeStrategy; + _feeStrategy = feeStrategy; + emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); + } + + /** + * @dev Updates Exposure Cap + * @param exposureCap The value of the new Exposure Cap + */ + function _updateExposureCap(uint128 exposureCap) internal { + uint128 oldExposureCap = _exposureCap; + _exposureCap = exposureCap; + emit ExposureCapUpdated(oldExposureCap, exposureCap); + } + + /** + * @dev Updates GHO Treasury Address + * @param newGhoTreasury The address of the new GHO Treasury + */ + function _updateGhoTreasury(address newGhoTreasury) internal { + require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); + address oldGhoTreasury = _ghoTreasury; + _ghoTreasury = newGhoTreasury; + emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return GSM_REVISION(); + } +} diff --git a/src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol b/src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol new file mode 100644 index 00000000..2dad5c5a --- /dev/null +++ b/src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol @@ -0,0 +1,38 @@ +pragma solidity ^0.8.10; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +/** + * @title MockIssuanceReceiverFailedInvalidUSDCAccepted + * @dev During issuance, the contract does not accept the proper amount of USDC but issues asset properly + */ +contract MockIssuanceReceiverFailedInvalidUSDCAccepted { + using SafeERC20 for IERC20; + + address public immutable asset; + address public immutable liquidity; + + /** + * @param _asset Address of asset token, ie BUIDL + * @param _liquidity Address of liquidity token, ie USDC + */ + constructor(address _asset, address _liquidity) { + asset = _asset; + liquidity = _liquidity; + } + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + + /** + * @notice Issue the asset token in exchange for selling the liquidity token + */ + function issuance(uint256 amount) external { + // TRIGGER ERROR: no enough payment token retrieved from msg.sender + IERC20(liquidity).safeTransferFrom(msg.sender, address(this), amount - 1); + IERC20(asset).safeTransfer(msg.sender, amount); + } +} From 71b8641eeee494f56f5c127a010bfadac65cb79b Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 17:48:43 -0500 Subject: [PATCH 43/68] refactor: rename redeemable asset to issued asset to be more correct --- .../gsm/converter/GsmConverter.sol | 42 +++++++-------- .../converter/interfaces/IGsmConverter.sol | 10 ++-- src/test/TestGhoBase.t.sol | 6 +-- src/test/TestGsmConverter.t.sol | 52 +++++++++---------- src/test/helpers/Events.sol | 2 +- ...MockRedemptionFailedIssuedAssetAmount.sol} | 4 +- 6 files changed, 56 insertions(+), 60 deletions(-) rename src/test/mocks/{MockRedemptionFailedRedeemableAssetAmount.sol => MockRedemptionFailedIssuedAssetAmount.sol} (93%) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 883c0f2d..9a49359f 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -41,7 +41,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address public immutable GSM; /// @inheritdoc IGsmConverter - address public immutable REDEEMABLE_ASSET; + address public immutable ISSUED_ASSET; /// @inheritdoc IGsmConverter address public immutable REDEEMED_ASSET; @@ -60,7 +60,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { * @param gsm The address of the associated GSM contract * @param redemptionContract The address of the redemption contract associated with the asset conversion * @param issuanceReceiverContract The address of the contract receiving the payment associated with the asset conversion - * @param redeemableAsset The address of the asset being redeemed + * @param issuedAsset The address of the asset being redeemed * @param redeemedAsset The address of the asset being received from redemption */ constructor( @@ -68,20 +68,20 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address gsm, address redemptionContract, address issuanceReceiverContract, - address redeemableAsset, + address issuedAsset, address redeemedAsset ) EIP712('GSMConverter', '1') { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(gsm != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redemptionContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(issuanceReceiverContract != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(redeemableAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(issuedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); GSM = gsm; REDEMPTION_CONTRACT = redemptionContract; ISSUANCE_RECEIVER_CONTRACT = issuanceReceiverContract; - REDEEMABLE_ASSET = redeemableAsset; // BUIDL + ISSUED_ASSET = issuedAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); @@ -177,40 +177,40 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address receiver ) internal returns (uint256, uint256) { uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); - uint256 initialRedeemableAssetBalance = IERC20(REDEEMABLE_ASSET).balanceOf(address(this)); + uint256 initialissuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); - (uint256 redeemableAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); + (uint256 issuedAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); IGhoToken(GHO_TOKEN).approve(address(GSM), 0); - IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), redeemableAssetAmount); - IRedemption(REDEMPTION_CONTRACT).redeem(redeemableAssetAmount); - // redeemedAssetAmount matches redeemableAssetAmount because Redemption exchanges in 1:1 ratio + IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), issuedAssetAmount); + IRedemption(REDEMPTION_CONTRACT).redeem(issuedAssetAmount); + // redeemedAssetAmount matches issuedAssetAmount because Redemption exchanges in 1:1 ratio require( IERC20(REDEEMED_ASSET).balanceOf(address(this)) == - initialRedeemedAssetBalance + redeemableAssetAmount, + initialRedeemedAssetBalance + issuedAssetAmount, 'INVALID_REDEMPTION' ); - IERC20(REDEEMABLE_ASSET).approve(address(REDEMPTION_CONTRACT), 0); + IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), 0); - IERC20(REDEEMED_ASSET).safeTransfer(receiver, redeemableAssetAmount); + IERC20(REDEEMED_ASSET).safeTransfer(receiver, issuedAssetAmount); require( IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, 'INVALID_REMAINING_GHO_BALANCE' ); require( - IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == initialRedeemableAssetBalance, - 'INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE' + IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialissuedAssetBalance, + 'INVALID_REMAINING_ISSUED_ASSET_BALANCE' ); - emit BuyAssetThroughRedemption(originator, receiver, redeemableAssetAmount, ghoSold); - return (redeemableAssetAmount, ghoSold); + emit BuyAssetThroughRedemption(originator, receiver, issuedAssetAmount, ghoSold); + return (issuedAssetAmount, ghoSold); } /** @@ -227,7 +227,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address receiver ) internal returns (uint256, uint256) { uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); - uint256 initialRedeemableAssetBalance = IERC20(REDEEMABLE_ASSET).balanceOf(address(this)); + uint256 initialissuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (uint256 redeemedAssetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); @@ -236,17 +236,17 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { //TODO: replace with proper issuance implementation later MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); require( - IERC20(REDEEMABLE_ASSET).balanceOf(address(this)) == + IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance + redeemedAssetAmount, 'INVALID_ISSUANCE' ); // reset approval after issuance IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, 0); - IERC20(REDEEMABLE_ASSET).approve(GSM, redeemedAssetAmount); + IERC20(ISSUED_ASSET).approve(GSM, redeemedAssetAmount); (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); // reset approval after sellAsset - IERC20(REDEEMABLE_ASSET).approve(GSM, 0); + IERC20(ISSUED_ASSET).approve(GSM, 0); require( IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index e867d4ad..d9c4e104 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -12,13 +12,13 @@ interface IGsmConverter { * @dev Emitted when a user buys an asset (selling GHO) in the GSM after a redemption * @param originator The address of the buyer originating the request * @param receiver The address of the receiver of the underlying asset - * @param redeemableAssetAmount The amount of the redeemable asset converted + * @param issuedAssetAmount The amount of the issued asset converted * @param ghoAmount The amount of total GHO sold, inclusive of fee */ event BuyAssetThroughRedemption( address indexed originator, address indexed receiver, - uint256 redeemableAssetAmount, + uint256 issuedAssetAmount, uint256 ghoAmount ); @@ -125,10 +125,10 @@ interface IGsmConverter { function GSM() external view returns (address); /** - * @notice Returns the address of the redeemable asset (token) associated with the converter - * @return The address of the redeemable asset + * @notice Returns the address of the issued asset (token) associated with the converter + * @return The address of the issued asset */ - function REDEEMABLE_ASSET() external view returns (address); + function ISSUED_ASSET() external view returns (address); /** * @notice Returns the address of the redeemed asset (token) associated with the converter diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index e8094666..a9db44f2 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -33,7 +33,7 @@ import {PriceOracle} from '@aave/core-v3/contracts/mocks/oracle/PriceOracle.sol' import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/TestnetERC20.sol'; import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; -import {MockRedemptionFailedRedeemableAssetAmount} from './mocks/MockRedemptionFailedRedeemableAssetAmount.sol'; +import {MockRedemptionFailedIssuedAssetAmount} from './mocks/MockRedemptionFailedIssuedAssetAmount.sol'; import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; import {MockIssuanceReceiverFailed} from './mocks/MockIssuanceReceiverFailed.sol'; @@ -119,7 +119,7 @@ contract TestGhoBase is Test, Constants, Events { MockAddressesProvider PROVIDER; MockConfigurator CONFIGURATOR; MockRedemption BUIDL_USDC_REDEMPTION; - MockRedemptionFailedRedeemableAssetAmount BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT; + MockRedemptionFailedIssuedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT; MockRedemptionFailed BUIDL_USDC_REDEMPTION_FAILED; MockIssuanceReceiver BUIDL_USDC_ISSUANCE; MockIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; @@ -353,7 +353,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_STEWARD_V2.setControlledFacilitator(controlledFacilitators, true); BUIDL_USDC_REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); - BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT = new MockRedemptionFailedRedeemableAssetAmount( + BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT = new MockRedemptionFailedIssuedAssetAmount( address(BUIDL_TOKEN), address(USDC_TOKEN) ); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 1ce9e966..1527cc94 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -33,11 +33,7 @@ contract TestGsmConverter is TestGhoBase { address(BUIDL_USDC_ISSUANCE), 'Unexpected issuance receiver contract address' ); - assertEq( - gsmConverter.REDEEMABLE_ASSET(), - address(BUIDL_TOKEN), - 'Unexpected redeemable asset address' - ); + assertEq(gsmConverter.ISSUED_ASSET(), address(BUIDL_TOKEN), 'Unexpected issued asset address'); assertEq( gsmConverter.REDEEMED_ASSET(), address(USDC_TOKEN), @@ -109,21 +105,21 @@ contract TestGsmConverter is TestGhoBase { function testSellAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(FAUCET); // Supply USDC to buyer - USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + USDC_TOKEN.mint(ALICE, expectedIssuedAssetAmount); // Supply BUIDL to issuance contract - BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); vm.stopPrank(); vm.startPrank(ALICE); - USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedRedeemableAssetAmount, expectedGhoBought); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, ALICE @@ -131,13 +127,13 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); - assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); assertEq( BUIDL_TOKEN.balanceOf(ALICE), 0, - 'Unexpected seller final BUIDL (redeemable asset) balance' + 'Unexpected seller final BUIDL (issued asset) balance' ); assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( @@ -172,7 +168,7 @@ contract TestGsmConverter is TestGhoBase { ); assertEq( USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), - expectedRedeemableAssetAmount, + expectedIssuedAssetAmount, 'Unexpected Issuance final USDC balance' ); assertEq( @@ -184,21 +180,21 @@ contract TestGsmConverter is TestGhoBase { function testSellAssetSendToOther() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemableAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(FAUCET); // Supply USDC to buyer - USDC_TOKEN.mint(ALICE, expectedRedeemableAssetAmount); + USDC_TOKEN.mint(ALICE, expectedIssuedAssetAmount); // Supply BUIDL to issuance contract - BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedRedeemableAssetAmount); + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); vm.stopPrank(); vm.startPrank(ALICE); - USDC_TOKEN.approve(address(GSM_CONVERTER), expectedRedeemableAssetAmount); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, BOB, expectedRedeemableAssetAmount, expectedGhoBought); + emit SellAssetThroughIssuance(ALICE, BOB, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, BOB @@ -206,20 +202,20 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); - assertEq(assetAmount, expectedRedeemableAssetAmount, 'Unexpected asset amount sold'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(BOB), 0, 'Unexpected receiver final USDC balance'); assertEq(GHO_TOKEN.balanceOf(BOB), ghoBought, 'Unexpected receiver final GHO balance'); assertEq( BUIDL_TOKEN.balanceOf(BOB), 0, - 'Unexpected receiver final BUIDL (redeemable asset) balance' + 'Unexpected receiver final BUIDL (issued asset) balance' ); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); assertEq( BUIDL_TOKEN.balanceOf(ALICE), 0, - 'Unexpected seller final BUIDL (redeemable asset) balance' + 'Unexpected seller final BUIDL (issued asset) balance' ); assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( @@ -254,7 +250,7 @@ contract TestGsmConverter is TestGhoBase { ); assertEq( USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), - expectedRedeemableAssetAmount, + expectedIssuedAssetAmount, 'Unexpected Issuance final USDC balance' ); assertEq( @@ -342,7 +338,7 @@ contract TestGsmConverter is TestGhoBase { } // TODO: Add test for sellAssetWithSig - // TODO: test for buyAsset failed redeemable asset amount? + // TODO: test for buyAsset failed issued asset amount? function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); @@ -595,11 +591,11 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } - function testRevertBuyAssetInvalidRemainingRedeemableAssetBalance() public { + function testRevertBuyAssetInvalidRemainingIssuedAssetBalance() public { GsmConverter gsmConverter = new GsmConverter( address(this), address(GHO_BUIDL_GSM), - address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT), + address(BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT), address(BUIDL_USDC_ISSUANCE), address(BUIDL_TOKEN), address(USDC_TOKEN) @@ -621,7 +617,7 @@ contract TestGsmConverter is TestGhoBase { // Supply USDC to the Redemption contract vm.prank(FAUCET); USDC_TOKEN.mint( - address(BUIDL_USDC_REDEMPTION_FAILED_REDEEMABLE_ASSET_AMOUNT), + address(BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT), DEFAULT_GSM_BUIDL_AMOUNT ); @@ -631,7 +627,7 @@ contract TestGsmConverter is TestGhoBase { GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); // Buy assets via Redemption of USDC - vm.expectRevert('INVALID_REMAINING_REDEEMABLE_ASSET_BALANCE'); + vm.expectRevert('INVALID_REMAINING_ISSUED_ASSET_BALANCE'); gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); vm.stopPrank(); } @@ -1281,7 +1277,7 @@ contract TestGsmConverter is TestGhoBase { assertEq(USDC_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC balance after'); } - function testRescueRedeemableTokens() public { + function testRescueIssuedTokens() public { vm.prank(FAUCET); BUIDL_TOKEN.mint(address(GSM_CONVERTER), DEFAULT_GSM_USDC_AMOUNT); diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index 85235b8b..aa6193e9 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -118,7 +118,7 @@ interface Events { event BuyAssetThroughRedemption( address indexed originator, address indexed receiver, - uint256 redeemableAssetAmount, + uint256 issuedAssetAmount, uint256 ghoAmount ); event SellAssetThroughIssuance( diff --git a/src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol b/src/test/mocks/MockRedemptionFailedIssuedAssetAmount.sol similarity index 93% rename from src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol rename to src/test/mocks/MockRedemptionFailedIssuedAssetAmount.sol index 3e2c6e42..5412810b 100644 --- a/src/test/mocks/MockRedemptionFailedRedeemableAssetAmount.sol +++ b/src/test/mocks/MockRedemptionFailedIssuedAssetAmount.sol @@ -23,11 +23,11 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockRedemptionFailedRedeemableAssetAmount + * @title MockRedemptionFailedIssuedAssetAmount * @dev Asset token is ERC20-compatible * @dev Liquidity token is ERC20-compatible */ -contract MockRedemptionFailedRedeemableAssetAmount is IRedemption { +contract MockRedemptionFailedIssuedAssetAmount is IRedemption { using SafeERC20 for IERC20; /** From 43187e6e26223071f291c9af94db2453f2af169a Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 18:01:54 -0500 Subject: [PATCH 44/68] test: declare new typehash specific for converter, init happy path sellAssetWithSig test --- src/test/TestGsmConverter.t.sol | 120 +++++++++++++++++++++++++++++--- src/test/helpers/Constants.sol | 10 +++ 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 1527cc94..6a05c564 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -337,8 +337,112 @@ contract TestGsmConverter is TestGhoBase { gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); } - // TODO: Add test for sellAssetWithSig - // TODO: test for buyAsset failed issued asset amount? + function testSellAssetWithSig() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 1 hours; + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedIssuedAssetAmount, + expectedGhoBought + ); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + + // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + // assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); + // assertEq( + // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + // DEFAULT_GSM_BUIDL_AMOUNT, + // 'Unexpected buyer final USDC balance' + // ); + // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + // assertEq( + // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected converter final USDC balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), + // 0, + // 'Unexpected buyer final GHO balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, + // 'Unexpected GSM final GHO balance' + // ); + // assertEq( + // GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected GSM_CONVERTER final GHO balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + // 0, + // 'Unexpected buyer final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + // 0, + // 'Unexpected GSM final BUIDL balance' + // ); + // assertEq( + // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + // 0, + // 'Unexpected GSM_CONVERTER final BUIDL balance' + // ); + } + + // TODO: test for buyAsset, check assertions on every balance function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); @@ -753,7 +857,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, DEFAULT_GSM_BUIDL_AMOUNT, @@ -867,7 +971,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, DEFAULT_GSM_BUIDL_AMOUNT, @@ -978,7 +1082,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, minAssetAmount, @@ -1083,7 +1187,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, DEFAULT_GSM_BUIDL_AMOUNT, @@ -1143,7 +1247,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, DEFAULT_GSM_BUIDL_AMOUNT, @@ -1203,7 +1307,7 @@ contract TestGsmConverter is TestGhoBase { abi.encode( '\x19\x01', GSM_CONVERTER.DOMAIN_SEPARATOR(), - GSM_BUY_ASSET_WITH_SIG_TYPEHASH, + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmConverterSignerAddr, DEFAULT_GSM_BUIDL_AMOUNT, diff --git a/src/test/helpers/Constants.sol b/src/test/helpers/Constants.sol index 98dd2770..a4f45ba1 100644 --- a/src/test/helpers/Constants.sol +++ b/src/test/helpers/Constants.sol @@ -36,6 +36,16 @@ contract Constants { 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' ); + // signature typehash for GSM Converter + bytes32 public constant GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + bytes32 public constant GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH = + keccak256( + 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' + ); + // defaults used in test environment uint256 constant DEFAULT_FLASH_FEE = 0.0009e4; // 0.09% uint128 constant DEFAULT_CAPACITY = 100_000_000e18; From cd76351f38528284bf06034d28709e4c8511996c Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 18:51:44 -0500 Subject: [PATCH 45/68] refactor: reference mock contract directly test: add assertions for testSellAssetWithSig --- remappings.txt | 1 - .../gsm/converter/GsmConverter.sol | 3 +- src/test/TestGsmConverter.t.sol | 109 +++++++++++------- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/remappings.txt b/remappings.txt index 7c6ed2e7..e7ac529f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -15,4 +15,3 @@ aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-periphery/ erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests/ openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/ solidity-utils/=lib/solidity-utils/src/ -mocks/=src/test/mocks/ diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 9a49359f..08314526 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -10,7 +10,8 @@ import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; -import {MockIssuanceReceiver} from 'mocks/MockIssuanceReceiver.sol'; +// TODO: replace with proper issuance implementation later +import {MockIssuanceReceiver} from '../../../../test/mocks/MockIssuanceReceiver.sol'; import 'forge-std/console2.sol'; diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 6a05c564..272fcd7b 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -397,52 +397,75 @@ contract TestGsmConverter is TestGhoBase { ); vm.stopPrank(); - // assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - // assertEq(redeemedUSDCAmount, DEFAULT_GSM_BUIDL_AMOUNT, 'Unexpected redeemed buyAsset amount'); - // assertEq( - // USDC_TOKEN.balanceOf(gsmConverterSignerAddr), - // DEFAULT_GSM_BUIDL_AMOUNT, - // 'Unexpected buyer final USDC balance' - // ); - // assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); - // assertEq( - // USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected converter final USDC balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(gsmConverterSignerAddr)), - // 0, - // 'Unexpected buyer final GHO balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT) + buyFee, - // 'Unexpected GSM final GHO balance' - // ); - // assertEq( - // GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected GSM_CONVERTER final GHO balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), - // 0, - // 'Unexpected buyer final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), - // 0, - // 'Unexpected GSM final BUIDL balance' - // ); - // assertEq( - // BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), - // 0, - // 'Unexpected GSM_CONVERTER final BUIDL balance' - // ); + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected seller final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(gsmConverterSignerAddr), + ghoBought, + 'Unexpected seller final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); } // TODO: test for buyAsset, check assertions on every balance + // TODO: test for buyAsset/withsig - when tokens are supplied to the contract function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); From cb7b33b1992c8962bbdb5049f10bd582d65e3c37 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 18:58:46 -0500 Subject: [PATCH 46/68] refactor: re-order assertions test: clean up referenced amounts in tests --- src/test/TestGsmConverter.t.sol | 87 +++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 272fcd7b..befa338b 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -402,17 +402,17 @@ contract TestGsmConverter is TestGhoBase { assertEq( USDC_TOKEN.balanceOf(gsmConverterSignerAddr), 0, - 'Unexpected seller final USDC balance' + 'Unexpected signer final USDC balance' ); assertEq( GHO_TOKEN.balanceOf(gsmConverterSignerAddr), ghoBought, - 'Unexpected seller final GHO balance' + 'Unexpected signer final GHO balance' ); assertEq( BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), 0, - 'Unexpected seller final BUIDL (issued asset) balance' + 'Unexpected signer final BUIDL (issued asset) balance' ); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); @@ -465,27 +465,25 @@ contract TestGsmConverter is TestGhoBase { } // TODO: test for buyAsset, check assertions on every balance - // TODO: test for buyAsset/withsig - when tokens are supplied to the contract + // TODO: test for buyAsset/withsig - when tokens are directly sent to the contract function testBuyAsset() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); - // USDC is redeemed for BUIDL in 1:1 ratio - uint256 expectedUSDCAmount = DEFAULT_GSM_BUIDL_AMOUNT; // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); - BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); vm.startPrank(ALICE); - BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); // Supply USDC to the Redemption contract vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedUSDCAmount); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); // Supply assets to another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); @@ -502,30 +500,38 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - assertEq(redeemedUSDCAmount, expectedUSDCAmount, 'Unexpected redeemed buyAsset amount'); - assertEq(USDC_TOKEN.balanceOf(BOB), expectedUSDCAmount, 'Unexpected buyer final USDC balance'); - assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( - USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - 0, - 'Unexpected converter final USDC balance' + redeemedUSDCAmount, + expectedRedeemedAssetAmount, + 'Unexpected redeemed buyAsset amount' + ); + assertEq( + USDC_TOKEN.balanceOf(BOB), + expectedRedeemedAssetAmount, + 'Unexpected buyer final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), sellFee + buyFee, 'Unexpected GSM final GHO balance' ); assertEq( - GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, - 'Unexpected GSM_CONVERTER final GHO balance' + 'Unexpected GSM final BUIDL balance' ); - assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); assertEq( - BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected GSM final BUIDL balance' + 'Unexpected converter final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' ); assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), @@ -539,20 +545,18 @@ contract TestGsmConverter is TestGhoBase { uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); - // USDC is redeemed for BUIDL in 1:1 ratio - uint256 expectedUSDCAmount = DEFAULT_GSM_BUIDL_AMOUNT; // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); - BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); vm.startPrank(ALICE); - BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); // Supply USDC to the Redemption contract vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedUSDCAmount); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); // Supply assets to another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); @@ -569,34 +573,41 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); - assertEq(redeemedUSDCAmount, expectedUSDCAmount, 'Unexpected redeemed buyAsset amount'); assertEq( - USDC_TOKEN.balanceOf(CHARLES), - expectedUSDCAmount, - 'Unexpected buyer final USDC balance' + redeemedUSDCAmount, + expectedRedeemedAssetAmount, + 'Unexpected redeemed buyAsset amount' ); - assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( - USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), - 0, - 'Unexpected converter final USDC balance' + USDC_TOKEN.balanceOf(CHARLES), + expectedRedeemedAssetAmount, + 'Unexpected buyer final USDC balance' ); + assertEq(GHO_TOKEN.balanceOf(address(CHARLES)), 0, 'Unexpected receiver final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(CHARLES), 0, 'Unexpected receiver final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(BOB), 0, 'Unexpected receiver final USDC balance'); assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), sellFee + buyFee, 'Unexpected GSM final GHO balance' ); assertEq( - GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, - 'Unexpected GSM_CONVERTER final GHO balance' + 'Unexpected GSM final BUIDL balance' ); - assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); assertEq( - BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), 0, - 'Unexpected GSM final BUIDL balance' + 'Unexpected converter final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' ); assertEq( BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), From 15e862ab6ba50e3a27a22facabed83ffae645f99 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 10 Sep 2024 19:03:21 -0500 Subject: [PATCH 47/68] test: fail cases for sellAssetWithSig --- src/test/TestGsmConverter.t.sol | 231 ++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index befa338b..861118fc 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -464,6 +464,237 @@ contract TestGsmConverter is TestGhoBase { ); } + function testSellAssetWithSigExactDeadline() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp; + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedIssuedAssetAmount, + expectedGhoBought + ); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected signer final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(gsmConverterSignerAddr), + ghoBought, + 'Unexpected signer final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected signer final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + + function testRevertSellAssetWithSigExpiredSignature() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp - 1; + (uint256 expectedIssuedAssetAmount, , , ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectRevert('SIGNATURE_DEADLINE_EXPIRED'); + GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + } + + function testRevertSellAssetWithSigInvalidSignature() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 10; + (uint256 expectedIssuedAssetAmount, , , ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.sellAssetWithSig(ALICE, DEFAULT_GSM_BUIDL_AMOUNT, ALICE, deadline, signature); + vm.stopPrank(); + } + // TODO: test for buyAsset, check assertions on every balance // TODO: test for buyAsset/withsig - when tokens are directly sent to the contract From 409944b9772c93f1962e37fca1b208a75051b88f Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:13:54 -0500 Subject: [PATCH 48/68] refactor: capitalize variable name --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 08314526..d0ad11da 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -178,7 +178,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address receiver ) internal returns (uint256, uint256) { uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); - uint256 initialissuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); + uint256 initialIssuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); @@ -206,7 +206,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { 'INVALID_REMAINING_GHO_BALANCE' ); require( - IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialissuedAssetBalance, + IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialIssuedAssetBalance, 'INVALID_REMAINING_ISSUED_ASSET_BALANCE' ); From 7b96c4cd4c0d38902d4eab2bfa6aab6c98c8d91b Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:16:38 -0500 Subject: [PATCH 49/68] refactor: use more precise variable reference --- src/test/TestGsmConverter.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 861118fc..b19b19e5 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1328,15 +1328,15 @@ contract TestGsmConverter is TestGhoBase { // Supply BUIDL assets to the BUIDL GSM first vm.prank(FAUCET); - BUIDL_TOKEN.mint(ALICE, minAssetAmount); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); vm.startPrank(ALICE); - BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), minAssetAmount); - GHO_BUIDL_GSM.sellAsset(minAssetAmount, ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + GHO_BUIDL_GSM.sellAsset(expectedRedeemedAssetAmount, ALICE); vm.stopPrank(); // Supply USDC to the Redemption contract vm.prank(FAUCET); - USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), minAssetAmount); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); // Supply assets to another user ghoFaucet(gsmConverterSignerAddr, expectedGhoSold); From 74da86e9602301e29f51cd5011e44b5e1551033f Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:19:29 -0500 Subject: [PATCH 50/68] test: add testRevertSellAssetWithSigInvalidAmount --- src/test/TestGsmConverter.t.sol | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index b19b19e5..0db5f9ab 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -695,6 +695,61 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } + function testRevertSellAssetWithSigInvalidAmount() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 10; + (uint256 expectedIssuedAssetAmount, , , ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT - 1, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + } + // TODO: test for buyAsset, check assertions on every balance // TODO: test for buyAsset/withsig - when tokens are directly sent to the contract From f0c35fd293c8d6d595d0dc16c804a11d567e15be Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:27:07 -0500 Subject: [PATCH 51/68] test: add testBuyAssetWithDonatedTokens --- src/test/TestGsmConverter.t.sol | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 0db5f9ab..a3bd92c5 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -902,6 +902,87 @@ contract TestGsmConverter is TestGhoBase { ); } + function testBuyAssetWithDonatedTokens() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + uint256 donatedAmount = 1e6; + + // Supply BUIDL assets to the BUIDL GSM first + vm.startPrank(FAUCET); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); + // donate tokens to the converter + BUIDL_TOKEN.mint(address(GSM_CONVERTER), donatedAmount); + USDC_TOKEN.mint(address(GSM_CONVERTER), donatedAmount); + vm.stopPrank(); + ghoFaucet(address(GSM_CONVERTER), donatedAmount); + + // sellAsset to seed GSM + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); + + // Supply assets to another user + ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + BOB + ); + vm.stopPrank(); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq( + redeemedUSDCAmount, + expectedRedeemedAssetAmount, + 'Unexpected redeemed buyAsset amount' + ); + assertEq( + USDC_TOKEN.balanceOf(BOB), + expectedRedeemedAssetAmount, + 'Unexpected buyer final USDC balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + } + function testRevertBuyAssetZeroAmount() public { vm.expectRevert('INVALID_MIN_AMOUNT'); uint256 invalidAmount = 0; From 57df1d7455ee0293081ae5e31532b67096d9b840 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:29:27 -0500 Subject: [PATCH 52/68] test: add testSellAssetDonatedTokens --- src/test/TestGsmConverter.t.sol | 83 ++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index a3bd92c5..f9d8eb93 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -260,6 +260,87 @@ contract TestGsmConverter is TestGhoBase { ); } + function testSellAssetDonatedTokens() public { + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + uint256 donatedAmount = 1e6; + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(ALICE, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + // donate tokens to the converter + BUIDL_TOKEN.mint(address(GSM_CONVERTER), donatedAmount); + USDC_TOKEN.mint(address(GSM_CONVERTER), donatedAmount); + vm.stopPrank(); + ghoFaucet(address(GSM_CONVERTER), donatedAmount); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + ALICE + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedAmount, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + function testRevertSellAssetZeroAmount() public { vm.prank(ALICE); vm.expectRevert('INVALID_MAX_AMOUNT'); @@ -902,7 +983,7 @@ contract TestGsmConverter is TestGhoBase { ); } - function testBuyAssetWithDonatedTokens() public { + function testBuyAssetDonatedTokens() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM From 4b1ed4e5da9f1a161d87863ec9e414081ab8c4db Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 08:53:11 -0500 Subject: [PATCH 53/68] test: fuzz for donated token amounts --- .../gsm/converter/GsmConverter.sol | 3 +- src/test/TestGsmConverter.t.sol | 178 ++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index d0ad11da..4e11f57a 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -198,7 +198,6 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { 'INVALID_REDEMPTION' ); IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), 0); - IERC20(REDEEMED_ASSET).safeTransfer(receiver, issuedAssetAmount); require( @@ -238,7 +237,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); require( IERC20(ISSUED_ASSET).balanceOf(address(this)) == - initialRedeemedAssetBalance + redeemedAssetAmount, + initialissuedAssetBalance + redeemedAssetAmount, 'INVALID_ISSUANCE' ); // reset approval after issuance diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index f9d8eb93..e3e810bc 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -341,6 +341,94 @@ contract TestGsmConverter is TestGhoBase { ); } + function testFuzzSellAssetDonatedTokens( + uint256 donatedIssuedAsset, + uint256 donatedRedeemedAsset, + uint256 donatedGHO + ) public { + donatedIssuedAsset = bound(donatedIssuedAsset, 1, 1e27); + donatedRedeemedAsset = bound(donatedRedeemedAsset, 1, 1e27); + (uint256 bucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(FAUCET); + donatedGHO = bound(donatedGHO, 1, bucketCapacity); + + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(ALICE, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + // donate tokens to the converter + BUIDL_TOKEN.mint(address(GSM_CONVERTER), donatedIssuedAsset); + USDC_TOKEN.mint(address(GSM_CONVERTER), donatedRedeemedAsset); + vm.stopPrank(); + ghoFaucet(address(GSM_CONVERTER), donatedGHO); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + ALICE + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedRedeemedAsset, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedIssuedAsset, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedGHO, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + function testRevertSellAssetZeroAmount() public { vm.prank(ALICE); vm.expectRevert('INVALID_MAX_AMOUNT'); @@ -1064,6 +1152,96 @@ contract TestGsmConverter is TestGhoBase { ); } + function testFuzzBuyAssetDonatedTokens( + uint256 donatedIssuedAsset, + uint256 donatedRedeemedAsset, + uint256 donatedGHO + ) public { + donatedIssuedAsset = bound(donatedIssuedAsset, 1, 1e27); + donatedRedeemedAsset = bound(donatedRedeemedAsset, 1, 1e27); + (uint256 bucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(FAUCET); + // enough buffer for later minted gho + donatedGHO = bound(donatedGHO, 1, bucketCapacity / 2); + + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.startPrank(FAUCET); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); + // donate tokens to the converter + BUIDL_TOKEN.mint(address(GSM_CONVERTER), donatedIssuedAsset); + USDC_TOKEN.mint(address(GSM_CONVERTER), donatedRedeemedAsset); + vm.stopPrank(); + ghoFaucet(address(GSM_CONVERTER), donatedGHO); + + // sellAsset to seed GSM + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); + + // Supply assets to another user + ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset( + DEFAULT_GSM_BUIDL_AMOUNT, + BOB + ); + vm.stopPrank(); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq( + redeemedUSDCAmount, + expectedRedeemedAssetAmount, + 'Unexpected redeemed buyAsset amount' + ); + assertEq( + USDC_TOKEN.balanceOf(BOB), + expectedRedeemedAssetAmount, + 'Unexpected buyer final USDC balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedRedeemedAsset, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedGHO, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + donatedIssuedAsset, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + } + function testRevertBuyAssetZeroAmount() public { vm.expectRevert('INVALID_MIN_AMOUNT'); uint256 invalidAmount = 0; From fdc083e26312d0942a847e98910010c968701a59 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 09:38:20 -0500 Subject: [PATCH 54/68] test: clean up var references and add testFuzzSellAssetMaxAmount --- src/test/TestGsmConverter.t.sol | 95 ++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index e3e810bc..7b7e9912 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -104,9 +104,12 @@ contract TestGsmConverter is TestGhoBase { } function testSellAsset() public { - uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM - .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + ( + uint256 expectedIssuedAssetAmount, + uint256 expectedGhoBought, + , + uint256 sellFee + ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(FAUCET); // Supply USDC to buyer @@ -179,9 +182,12 @@ contract TestGsmConverter is TestGhoBase { } function testSellAssetSendToOther() public { - uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM - .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + ( + uint256 expectedIssuedAssetAmount, + uint256 expectedGhoBought, + , + uint256 sellFee + ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(FAUCET); // Supply USDC to buyer @@ -341,6 +347,83 @@ contract TestGsmConverter is TestGhoBase { ); } + function testFuzzSellAssetMaxAmount(uint256 maxAmount) public { + // bound to some multiple of the default amount + maxAmount = bound(maxAmount, 1, GHO_BUIDL_GSM.getExposureCap()); + ( + uint256 expectedIssuedAssetAmount, + uint256 expectedGhoBought, + , + uint256 sellFee + ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(maxAmount); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(ALICE, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset(maxAmount, ALICE); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + function testFuzzSellAssetDonatedTokens( uint256 donatedIssuedAsset, uint256 donatedRedeemedAsset, From df84a215ca0283ce004baf581afb50934d69dc96 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 09:44:46 -0500 Subject: [PATCH 55/68] test: clean up var reference --- src/test/TestGsmConverter.t.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 7b7e9912..dbc77034 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1006,9 +1006,7 @@ contract TestGsmConverter is TestGhoBase { // TODO: test for buyAsset/withsig - when tokens are directly sent to the contract function testBuyAsset() public { - uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); // Supply BUIDL assets to the BUIDL GSM first @@ -1016,6 +1014,7 @@ contract TestGsmConverter is TestGhoBase { BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); vm.startPrank(ALICE); BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + (, , , uint256 sellFee) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); @@ -1079,9 +1078,7 @@ contract TestGsmConverter is TestGhoBase { } function testBuyAssetSendToOther() public { - uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); - uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); // Supply BUIDL assets to the BUIDL GSM first @@ -1089,6 +1086,7 @@ contract TestGsmConverter is TestGhoBase { BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); vm.startPrank(ALICE); BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + (, , , uint256 sellFee) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); vm.stopPrank(); From 91fb7b9e19d9fd792f5a28c518cabebc565b5c7b Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 10:01:17 -0500 Subject: [PATCH 56/68] test: add fuzz tests, testRevertSellAssetWithSigInvalidNonce --- src/test/TestGsmConverter.t.sol | 312 +++++++++++++++++++++++++++----- 1 file changed, 262 insertions(+), 50 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index dbc77034..2da6a218 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -348,7 +348,6 @@ contract TestGsmConverter is TestGhoBase { } function testFuzzSellAssetMaxAmount(uint256 maxAmount) public { - // bound to some multiple of the default amount maxAmount = bound(maxAmount, 1, GHO_BUIDL_GSM.getExposureCap()); ( uint256 expectedIssuedAssetAmount, @@ -843,6 +842,138 @@ contract TestGsmConverter is TestGhoBase { ); } + function testFuzzSellAssetWithSigSignature( + string memory randomStr, + uint256 deadlineBuffer + ) public { + deadlineBuffer = bound(deadlineBuffer, 0, 52 weeks); + + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey(randomStr); + + uint256 deadline = block.timestamp + deadlineBuffer; + uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); + (uint256 expectedIssuedAssetAmount, uint256 expectedGhoBought, , ) = GHO_BUIDL_GSM + .getGhoAmountForSellAsset(DEFAULT_GSM_BUIDL_AMOUNT); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit SellAssetThroughIssuance( + gsmConverterSignerAddr, + gsmConverterSignerAddr, + expectedIssuedAssetAmount, + expectedGhoBought + ); + (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + + assertEq(ghoBought, expectedGhoBought, 'Unexpected GHO bought amount'); + assertEq(assetAmount, expectedIssuedAssetAmount, 'Unexpected asset amount sold'); + assertEq( + USDC_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected signer final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(gsmConverterSignerAddr), + ghoBought, + 'Unexpected signer final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(gsmConverterSignerAddr), + 0, + 'Unexpected signer final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected seller final GHO balance'); + assertEq( + BUIDL_TOKEN.balanceOf(ALICE), + 0, + 'Unexpected seller final BUIDL (issued asset) balance' + ); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + assetAmount, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final USDC balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + expectedIssuedAssetAmount, + 'Unexpected Issuance final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(BUIDL_USDC_ISSUANCE)), + 0, + 'Unexpected Issuance final GHO balance' + ); + } + function testRevertSellAssetWithSigExpiredSignature() public { (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); @@ -1002,8 +1133,60 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } - // TODO: test for buyAsset, check assertions on every balance - // TODO: test for buyAsset/withsig - when tokens are directly sent to the contract + function testRevertSellAssetWithSigInvalidNonce() public { + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 10; + (uint256 expectedIssuedAssetAmount, , , ) = GHO_BUIDL_GSM.getGhoAmountForSellAsset( + DEFAULT_GSM_BUIDL_AMOUNT + ); + + vm.startPrank(FAUCET); + // Supply USDC to buyer + USDC_TOKEN.mint(gsmConverterSignerAddr, expectedIssuedAssetAmount); + // Supply BUIDL to issuance contract + BUIDL_TOKEN.mint(address(BUIDL_USDC_ISSUANCE), expectedIssuedAssetAmount); + vm.stopPrank(); + + vm.prank(gsmConverterSignerAddr); + USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_SELL_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr) + 1, // invalid nonce + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != ALICE, 'Signer is the same as Bob'); + + vm.prank(ALICE); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.sellAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + vm.stopPrank(); + } function testBuyAsset() public { (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM @@ -1023,9 +1206,9 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); // Supply assets to another user - ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + ghoFaucet(BOB, expectedGhoSold); vm.startPrank(BOB); - GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); // Buy assets via Redemption of USDC vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); @@ -1095,9 +1278,9 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); // Supply assets to another user - ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); + ghoFaucet(BOB, expectedGhoSold); vm.startPrank(BOB); - GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); // Buy assets via Redemption of USDC vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); @@ -1152,6 +1335,76 @@ contract TestGsmConverter is TestGhoBase { ); } + function testFuzzBuyAssetMinAmount(uint256 minAmount) public { + minAmount = bound(minAmount, 1, GHO_BUIDL_GSM.getExposureCap()); + (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM + .getGhoAmountForBuyAsset(minAmount); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, expectedRedeemedAssetAmount); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), expectedRedeemedAssetAmount); + (, , , uint256 sellFee) = GHO_BUIDL_GSM.getGhoAmountForSellAsset(minAmount); + GHO_BUIDL_GSM.sellAsset(minAmount, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), expectedRedeemedAssetAmount); + + // Supply assets to another user + ghoFaucet(BOB, expectedGhoSold); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(GSM_CONVERTER), expectedGhoSold); + + // Buy assets via Redemption of USDC + vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); + emit BuyAssetThroughRedemption(BOB, BOB, expectedRedeemedAssetAmount, expectedGhoSold); + (uint256 redeemedUSDCAmount, uint256 ghoSold) = GSM_CONVERTER.buyAsset(minAmount, BOB); + vm.stopPrank(); + + assertEq(ghoSold, expectedGhoSold, 'Unexpected GHO sold amount'); + assertEq( + redeemedUSDCAmount, + expectedRedeemedAssetAmount, + 'Unexpected redeemed buyAsset amount' + ); + assertEq( + USDC_TOKEN.balanceOf(BOB), + expectedRedeemedAssetAmount, + 'Unexpected buyer final USDC balance' + ); + assertEq(GHO_TOKEN.balanceOf(address(BOB)), 0, 'Unexpected buyer final GHO balance'); + assertEq(BUIDL_TOKEN.balanceOf(BOB), 0, 'Unexpected buyer final BUIDL balance'); + assertEq(USDC_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), 0, 'Unexpected GSM final USDC balance'); + assertEq( + GHO_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + sellFee + buyFee, + 'Unexpected GSM final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GHO_BUIDL_GSM)), + 0, + 'Unexpected GSM final BUIDL balance' + ); + assertEq( + USDC_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected converter final USDC balance' + ); + assertEq( + GHO_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final GHO balance' + ); + assertEq( + BUIDL_TOKEN.balanceOf(address(GSM_CONVERTER)), + 0, + 'Unexpected GSM_CONVERTER final BUIDL balance' + ); + } + function testBuyAssetDonatedTokens() public { uint256 sellFee = GHO_GSM_FIXED_FEE_STRATEGY.getSellFee(DEFAULT_GSM_GHO_AMOUNT); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); @@ -1519,49 +1772,6 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } - /// TODO: @dev Assuming an attacker donates BUIDL token to the converter - // function testRevertBuyAssetInvalidRedemptionNonZeroBalance() public { - // GsmConverter gsmConverter = new GsmConverter( - // address(this), - // address(GHO_BUIDL_GSM), - // address(BUIDL_USDC_REDEMPTION_FAILED), - // address(BUIDL_USDC_ISSUANCE), - // address(BUIDL_TOKEN), - // address(USDC_TOKEN) - // ); - - // uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); - // (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( - // DEFAULT_GSM_BUIDL_AMOUNT - // ); - - // // Supply BUIDL assets to the BUIDL GSM first - // vm.prank(FAUCET); - // BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); - // vm.startPrank(ALICE); - // BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); - // GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); - // vm.stopPrank(); - - // // Supply USDC to the Redemption contract - // vm.prank(FAUCET); - // uint256 bufferForAdditionalTransfer = 1000; - // USDC_TOKEN.mint( - // address(BUIDL_USDC_REDEMPTION_FAILED), - // DEFAULT_GSM_BUIDL_AMOUNT + bufferForAdditionalTransfer - // ); - - // // Supply assets to another user - // ghoFaucet(BOB, expectedGhoSold + buyFee); - // vm.startPrank(BOB); - // GHO_TOKEN.approve(address(gsmConverter), expectedGhoSold + buyFee); - - // // Invalid redemption of USDC - // vm.expectRevert('INVALID_REDEMPTION'); - // gsmConverter.buyAsset(DEFAULT_GSM_BUIDL_AMOUNT, BOB); - // vm.stopPrank(); - // } - function testBuyAssetWithSig() public { (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); @@ -2074,6 +2284,8 @@ contract TestGsmConverter is TestGhoBase { ); } + //TODO: testRevertBuyAssetWithSigInvalidNonce + function testRescueTokens() public { vm.prank(FAUCET); WETH.mint(address(GSM_CONVERTER), 100e18); From 655b0b0299ba70b6b05985d63397c6e1019ebbfd Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 10:03:29 -0500 Subject: [PATCH 57/68] test: add testRevertBuyAssetWithSigInvalidNonce --- src/test/TestGsmConverter.t.sol | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 2da6a218..164a6346 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -2284,7 +2284,65 @@ contract TestGsmConverter is TestGhoBase { ); } - //TODO: testRevertBuyAssetWithSigInvalidNonce + function testRevertBuyAssetWithSigInvalidNonce() public { + // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp + (gsmConverterSignerAddr, gsmConverterSignerKey) = makeAddrAndKey('randomString'); + + uint256 deadline = block.timestamp + 1; + uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); + + // Supply BUIDL assets to the BUIDL GSM first + vm.prank(FAUCET); + BUIDL_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); + vm.startPrank(ALICE); + BUIDL_TOKEN.approve(address(GHO_BUIDL_GSM), DEFAULT_GSM_BUIDL_AMOUNT); + GHO_BUIDL_GSM.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); + + // Supply USDC to the Redemption contract + vm.prank(FAUCET); + USDC_TOKEN.mint(address(BUIDL_USDC_REDEMPTION), DEFAULT_GSM_BUIDL_AMOUNT); + + // Supply assets to another user + ghoFaucet(gsmConverterSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); + vm.prank(gsmConverterSignerAddr); + GHO_TOKEN.approve(address(GSM_CONVERTER), DEFAULT_GSM_GHO_AMOUNT + buyFee); + + assertEq( + GSM_CONVERTER.nonces(gsmConverterSignerAddr), + 0, + 'Unexpected before gsmConverterSignerAddr nonce' + ); + + bytes32 digest = keccak256( + abi.encode( + '\x19\x01', + GSM_CONVERTER.DOMAIN_SEPARATOR(), + GSM_CONVERTER_BUY_ASSET_WITH_SIG_TYPEHASH, + abi.encode( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + GSM_CONVERTER.nonces(gsmConverterSignerAddr) + 1, // invalid nonce + deadline + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmConverterSignerKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue(gsmConverterSignerAddr != BOB, 'Signer is the same as Bob'); + + vm.prank(BOB); + vm.expectRevert('SIGNATURE_INVALID'); + GSM_CONVERTER.buyAssetWithSig( + gsmConverterSignerAddr, + DEFAULT_GSM_BUIDL_AMOUNT, + gsmConverterSignerAddr, + deadline, + signature + ); + } function testRescueTokens() public { vm.prank(FAUCET); From d0f89f200d0ca3cf2aa77c0a3560ba9bcfdb5190 Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 11 Sep 2024 10:10:47 -0500 Subject: [PATCH 58/68] test: remove mock test files from coverage --- src/test/mocks/MockERC4626.sol | 5 +++++ src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol | 5 +++++ src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol | 5 +++++ src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol | 5 +++++ src/test/mocks/MockUpgradeable.sol | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/src/test/mocks/MockERC4626.sol b/src/test/mocks/MockERC4626.sol index 74a0279b..e99b3b8e 100644 --- a/src/test/mocks/MockERC4626.sol +++ b/src/test/mocks/MockERC4626.sol @@ -11,4 +11,9 @@ contract MockERC4626 is ERC4626 { string memory symbol, address asset ) ERC4626(IERC20(asset)) ERC20(name, symbol) {} + + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } } diff --git a/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol index 087e1cae..c36c66a4 100644 --- a/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol +++ b/src/test/mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol @@ -88,6 +88,11 @@ contract MockGsmFailedBuyAssetRemainingGhoBalance is _; } + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + /** * @dev Constructor * @param ghoToken The address of the GHO token contract diff --git a/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol b/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol index b61b1da7..b71ffab7 100644 --- a/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol +++ b/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol @@ -88,6 +88,11 @@ contract MockGsmFailedGetGhoAmountForBuyAsset is _; } + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + /** * @dev Constructor * @param ghoToken The address of the GHO token contract diff --git a/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol index 7093746b..81831e4e 100644 --- a/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol +++ b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol @@ -88,6 +88,11 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is _; } + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + /** * @dev Constructor * @param ghoToken The address of the GHO token contract diff --git a/src/test/mocks/MockUpgradeable.sol b/src/test/mocks/MockUpgradeable.sol index 285679b0..a490a575 100644 --- a/src/test/mocks/MockUpgradeable.sol +++ b/src/test/mocks/MockUpgradeable.sol @@ -14,6 +14,11 @@ contract MockUpgradeable is Initializable { // Intentionally left bank } + function test_coverage_ignore() public virtual { + // Intentionally left blank. + // Excludes contract from coverage. + } + /** * @dev Initializer */ From 8a47c39840b11b6522f52f01211c2489d596d81c Mon Sep 17 00:00:00 2001 From: YBM Date: Wed, 18 Sep 2024 16:20:21 -0500 Subject: [PATCH 59/68] fix: remove need to upgrade gsm to trigger error --- src/test/TestGhoBase.t.sol | 1 - src/test/TestGsmConverter.t.sol | 28 +- .../MockGsmFailedGetGhoAmountForBuyAsset.sol | 582 ------------------ 3 files changed, 8 insertions(+), 603 deletions(-) delete mode 100644 src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index a9db44f2..5d9492c1 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -22,7 +22,6 @@ import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; -import {MockGsmFailedGetGhoAmountForBuyAsset} from './mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol'; import {MockGsmFailedBuyAssetRemainingGhoBalance} from './mocks/MockGsmFailedBuyAssetRemainingGhoBalance.sol'; import {MockGsmFailedSellAssetRemainingGhoBalance} from './mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol'; import {MockPool} from './mocks/MockPool.sol'; diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 164a6346..710870c2 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1628,7 +1628,14 @@ contract TestGsmConverter is TestGhoBase { } function testRevertBuyAssetInvalidGhoSold() public { - _upgradeToGsmFailedBuyAssetGhoAmount(); + vm.mockCall( + address(GHO_BUIDL_GSM), + abi.encodeWithSelector( + GHO_BUIDL_GSM.getGhoAmountForBuyAsset.selector, + DEFAULT_GSM_BUIDL_AMOUNT + ), + abi.encode(100000000, 110000000000000000001, 100000000000000000000, 10000000000000000000) + ); uint256 buyFee = GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); (, uint256 expectedGhoSold, , ) = GHO_BUIDL_GSM.getGhoAmountForBuyAsset( @@ -2407,25 +2414,6 @@ contract TestGsmConverter is TestGhoBase { ); } - function _upgradeToGsmFailedBuyAssetGhoAmount() internal { - address gsmFailed = address( - new MockGsmFailedGetGhoAmountForBuyAsset( - address(GHO_TOKEN), - address(BUIDL_TOKEN), - address(GHO_BUIDL_GSM_FIXED_PRICE_STRATEGY) - ) - ); - bytes memory data = abi.encodeWithSelector( - MockGsmFailedGetGhoAmountForBuyAsset.initialize.selector, - address(this), - TREASURY, - DEFAULT_GSM_USDC_EXPOSURE - ); - - vm.prank(SHORT_EXECUTOR); - AdminUpgradeabilityProxy(payable(address(GHO_BUIDL_GSM))).upgradeToAndCall(gsmFailed, data); - } - function _upgradeToGsmFailedBuyAssetRemainingGhoBalance() internal { address gsmFailed = address( new MockGsmFailedBuyAssetRemainingGhoBalance( diff --git a/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol b/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol deleted file mode 100644 index b71ffab7..00000000 --- a/src/test/mocks/MockGsmFailedGetGhoAmountForBuyAsset.sol +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; -import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; -import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; -import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; -import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; -import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; -import {IGhoFacilitator} from '../../contracts/gho/interfaces/IGhoFacilitator.sol'; -import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; -import {IGsmPriceStrategy} from '../../contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; -import {IGsmFeeStrategy} from '../../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; -import {IGsm} from '../../contracts/facilitators/gsm/interfaces/IGsm.sol'; - -/** - * @title MockGsmFailedGetGhoAmountForBuyAsset - * @author Aave - * @notice GSM that fails calculation for GHO amount in getGhoAmountForBuyAsset - */ -contract MockGsmFailedGetGhoAmountForBuyAsset is - AccessControl, - VersionedInitializable, - EIP712, - IGsm -{ - using GPv2SafeERC20 for IERC20; - using SafeCast for uint256; - - /// @inheritdoc IGsm - bytes32 public constant CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); - - /// @inheritdoc IGsm - bytes32 public constant TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); - - /// @inheritdoc IGsm - bytes32 public constant SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); - - /// @inheritdoc IGsm - bytes32 public constant LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); - - /// @inheritdoc IGsm - bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = - keccak256( - 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' - ); - - /// @inheritdoc IGsm - bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = - keccak256( - 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' - ); - - /// @inheritdoc IGsm - address public immutable GHO_TOKEN; - - /// @inheritdoc IGsm - address public immutable UNDERLYING_ASSET; - - /// @inheritdoc IGsm - address public immutable PRICE_STRATEGY; - - /// @inheritdoc IGsm - mapping(address => uint256) public nonces; - - address internal _ghoTreasury; - address internal _feeStrategy; - bool internal _isFrozen; - bool internal _isSeized; - uint128 internal _exposureCap; - uint128 internal _currentExposure; - uint128 internal _accruedFees; - - /** - * @dev Require GSM to not be frozen for functions marked by this modifier - */ - modifier notFrozen() { - require(!_isFrozen, 'GSM_FROZEN'); - _; - } - - /** - * @dev Require GSM to not be seized for functions marked by this modifier - */ - modifier notSeized() { - require(!_isSeized, 'GSM_SEIZED'); - _; - } - - function test_coverage_ignore() public virtual { - // Intentionally left blank. - // Excludes contract from coverage. - } - - /** - * @dev Constructor - * @param ghoToken The address of the GHO token contract - * @param underlyingAsset The address of the collateral asset - * @param priceStrategy The address of the price strategy - */ - constructor(address ghoToken, address underlyingAsset, address priceStrategy) EIP712('GSM', '1') { - require(ghoToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); - require( - IGsmPriceStrategy(priceStrategy).UNDERLYING_ASSET() == underlyingAsset, - 'INVALID_PRICE_STRATEGY' - ); - GHO_TOKEN = ghoToken; - UNDERLYING_ASSET = underlyingAsset; - PRICE_STRATEGY = priceStrategy; - } - - /** - * @notice GSM initializer - * @param admin The address of the default admin role - * @param ghoTreasury The address of the GHO treasury - * @param exposureCap Maximum amount of user-supplied underlying asset in GSM - */ - function initialize( - address admin, - address ghoTreasury, - uint128 exposureCap - ) external initializer { - require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); - _grantRole(DEFAULT_ADMIN_ROLE, admin); - _grantRole(CONFIGURATOR_ROLE, admin); - _updateGhoTreasury(ghoTreasury); - _updateExposureCap(exposureCap); - } - - /// @inheritdoc IGsm - function buyAsset( - uint256 minAmount, - address receiver - ) external notFrozen notSeized returns (uint256, uint256) { - return _buyAsset(msg.sender, minAmount, receiver); - } - - /// @inheritdoc IGsm - function buyAssetWithSig( - address originator, - uint256 minAmount, - address receiver, - uint256 deadline, - bytes calldata signature - ) external notFrozen notSeized returns (uint256, uint256) { - require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); - bytes32 digest = keccak256( - abi.encode( - '\x19\x01', - _domainSeparatorV4(), - BUY_ASSET_WITH_SIG_TYPEHASH, - abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) - ) - ); - require( - SignatureChecker.isValidSignatureNow(originator, digest, signature), - 'SIGNATURE_INVALID' - ); - - return _buyAsset(originator, minAmount, receiver); - } - - /// @inheritdoc IGsm - function sellAsset( - uint256 maxAmount, - address receiver - ) external notFrozen notSeized returns (uint256, uint256) { - return _sellAsset(msg.sender, maxAmount, receiver); - } - - /// @inheritdoc IGsm - function sellAssetWithSig( - address originator, - uint256 maxAmount, - address receiver, - uint256 deadline, - bytes calldata signature - ) external notFrozen notSeized returns (uint256, uint256) { - require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); - bytes32 digest = keccak256( - abi.encode( - '\x19\x01', - _domainSeparatorV4(), - SELL_ASSET_WITH_SIG_TYPEHASH, - abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) - ) - ); - require( - SignatureChecker.isValidSignatureNow(originator, digest, signature), - 'SIGNATURE_INVALID' - ); - - return _sellAsset(originator, maxAmount, receiver); - } - - /// @inheritdoc IGsm - function rescueTokens( - address token, - address to, - uint256 amount - ) external onlyRole(TOKEN_RESCUER_ROLE) { - require(amount > 0, 'INVALID_AMOUNT'); - if (token == GHO_TOKEN) { - uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; - require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); - } - if (token == UNDERLYING_ASSET) { - uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; - require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); - } - IERC20(token).safeTransfer(to, amount); - emit TokensRescued(token, to, amount); - } - - /// @inheritdoc IGsm - function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { - if (enable) { - require(!_isFrozen, 'GSM_ALREADY_FROZEN'); - } else { - require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); - } - _isFrozen = enable; - emit SwapFreeze(msg.sender, enable); - } - - /// @inheritdoc IGsm - function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { - _isSeized = true; - _currentExposure = 0; - _updateExposureCap(0); - - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); - if (underlyingBalance > 0) { - IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); - } - - emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); - return underlyingBalance; - } - - /// @inheritdoc IGsm - function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { - require(_isSeized, 'GSM_NOT_SEIZED'); - require(amount > 0, 'INVALID_AMOUNT'); - - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - if (amount > ghoMinted) { - amount = ghoMinted; - } - IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); - IGhoToken(GHO_TOKEN).burn(amount); - - emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); - return amount; - } - - /// @inheritdoc IGsm - function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { - _updateFeeStrategy(feeStrategy); - } - - /// @inheritdoc IGsm - function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { - _updateExposureCap(exposureCap); - } - - /// @inheritdoc IGhoFacilitator - function distributeFeesToTreasury() public virtual override { - uint256 accruedFees = _accruedFees; - if (accruedFees > 0) { - _accruedFees = 0; - IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); - emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); - } - } - - /// @inheritdoc IGhoFacilitator - function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { - _updateGhoTreasury(newGhoTreasury); - } - - /// @inheritdoc IGsm - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } - - /// @inheritdoc IGsm - function getGhoAmountForBuyAsset( - uint256 minAssetAmount - ) external view returns (uint256, uint256, uint256, uint256) { - ( - uint256 assetAmount, - uint256 ghoSold, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForBuyAsset(minAssetAmount); - // TRIGGER ERROR: invalid amount of ghoSold value returned - return (assetAmount, ghoSold * 2, grossAmount, fee); - } - - /// @inheritdoc IGsm - function getGhoAmountForSellAsset( - uint256 maxAssetAmount - ) external view returns (uint256, uint256, uint256, uint256) { - return _calculateGhoAmountForSellAsset(maxAssetAmount); - } - - /// @inheritdoc IGsm - function getAssetAmountForBuyAsset( - uint256 maxGhoAmount - ) external view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - uint256 grossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) - : maxGhoAmount; - // round down so maxGhoAmount is guaranteed - uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); - uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( - assetAmount, - true - ); - uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; - return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); - } - - /// @inheritdoc IGsm - function getAssetAmountForSellAsset( - uint256 minGhoAmount - ) external view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - uint256 grossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(minGhoAmount) - : minGhoAmount; - // round up so minGhoAmount is guaranteed - uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, true); - uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( - assetAmount, - false - ); - uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(finalGrossAmount) : 0; - return (assetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); - } - - /// @inheritdoc IGsm - function getAvailableUnderlyingExposure() external view returns (uint256) { - return _exposureCap > _currentExposure ? _exposureCap - _currentExposure : 0; - } - - /// @inheritdoc IGsm - function getExposureCap() external view returns (uint128) { - return _exposureCap; - } - - /// @inheritdoc IGsm - function getAvailableLiquidity() external view returns (uint256) { - return _currentExposure; - } - - /// @inheritdoc IGsm - function getFeeStrategy() external view returns (address) { - return _feeStrategy; - } - - /// @inheritdoc IGsm - function getAccruedFees() external view returns (uint256) { - return _accruedFees; - } - - /// @inheritdoc IGsm - function getIsFrozen() external view returns (bool) { - return _isFrozen; - } - - /// @inheritdoc IGsm - function getIsSeized() external view returns (bool) { - return _isSeized; - } - - /// @inheritdoc IGsm - function canSwap() external view returns (bool) { - return !_isFrozen && !_isSeized; - } - - /// @inheritdoc IGhoFacilitator - function getGhoTreasury() external view override returns (address) { - return _ghoTreasury; - } - - /// @inheritdoc IGsm - function GSM_REVISION() public pure virtual override returns (uint256) { - return 2; - } - - /** - * @dev Buys an underlying asset with GHO - * @param originator The originator of the request - * @param minAmount The minimum amount of the underlying asset desired for purchase - * @param receiver The recipient address of the underlying asset being purchased - * @return The amount of underlying asset bought - * @return The amount of GHO sold by the user - */ - function _buyAsset( - address originator, - uint256 minAmount, - address receiver - ) internal returns (uint256, uint256) { - ( - uint256 assetAmount, - uint256 ghoSold, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForBuyAsset(minAmount); - - _beforeBuyAsset(originator, assetAmount, receiver); - - require(assetAmount > 0, 'INVALID_AMOUNT'); - require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); - - _currentExposure -= uint128(assetAmount); - _accruedFees += fee.toUint128(); - // TRIGGER ERROR: transfer incorrect GHO amount to GSM - IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold - 1); - IGhoToken(GHO_TOKEN).burn(grossAmount); - IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); - - emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); - return (assetAmount, ghoSold); - } - - /** - * @dev Hook that is called before `buyAsset`. - * @dev This can be used to add custom logic - * @param originator Originator of the request - * @param amount The amount of the underlying asset desired for purchase - * @param receiver Recipient address of the underlying asset being purchased - */ - function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} - - /** - * @dev Sells an underlying asset for GHO - * @param originator The originator of the request - * @param maxAmount The maximum amount of the underlying asset desired to sell - * @param receiver The recipient address of the GHO being purchased - * @return The amount of underlying asset sold - * @return The amount of GHO bought by the user - */ - function _sellAsset( - address originator, - uint256 maxAmount, - address receiver - ) internal returns (uint256, uint256) { - ( - uint256 assetAmount, - uint256 ghoBought, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForSellAsset(maxAmount); - - _beforeSellAsset(originator, assetAmount, receiver); - - require(assetAmount > 0, 'INVALID_AMOUNT'); - require(_currentExposure + assetAmount <= _exposureCap, 'EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); - - _currentExposure += uint128(assetAmount); - _accruedFees += fee.toUint128(); - IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); - - IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); - IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); - - emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); - return (assetAmount, ghoBought); - } - - /** - * @dev Hook that is called before `sellAsset`. - * @dev This can be used to add custom logic - * @param originator Originator of the request - * @param amount The amount of the underlying asset desired to sell - * @param receiver Recipient address of the GHO being purchased - */ - function _beforeSellAsset( - address originator, - uint256 amount, - address receiver - ) internal virtual {} - - /** - * @dev Returns the amount of GHO sold in exchange of buying underlying asset - * @param assetAmount The amount of underlying asset to buy - * @return The exact amount of asset the user purchases - * @return The total amount of GHO the user sells (gross amount in GHO plus fee) - * @return The gross amount of GHO - * @return The fee amount in GHO, applied on top of gross amount of GHO - */ - function _calculateGhoAmountForBuyAsset( - uint256 assetAmount - ) internal view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - // pick the highest GHO amount possible for given asset amount - uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); - uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; - uint256 ghoSold = grossAmount + fee; - uint256 finalGrossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) - : ghoSold; - // pick the lowest asset amount possible for given GHO amount - uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( - finalGrossAmount, - false - ); - uint256 finalFee = ghoSold - finalGrossAmount; - return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); - } - - /** - * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset - * @param assetAmount The amount of underlying asset to sell - * @return The exact amount of asset the user sells - * @return The total amount of GHO the user buys (gross amount in GHO minus fee) - * @return The gross amount of GHO - * @return The fee amount in GHO, applied to the gross amount of GHO - */ - function _calculateGhoAmountForSellAsset( - uint256 assetAmount - ) internal view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - // pick the lowest GHO amount possible for given asset amount - uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, false); - uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(grossAmount) : 0; - uint256 ghoBought = grossAmount - fee; - uint256 finalGrossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(ghoBought) - : ghoBought; - // pick the highest asset amount possible for given GHO amount - uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( - finalGrossAmount, - true - ); - uint256 finalFee = finalGrossAmount - ghoBought; - return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); - } - - /** - * @dev Updates Fee Strategy - * @param feeStrategy The address of the new Fee Strategy - */ - function _updateFeeStrategy(address feeStrategy) internal { - address oldFeeStrategy = _feeStrategy; - _feeStrategy = feeStrategy; - emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); - } - - /** - * @dev Updates Exposure Cap - * @param exposureCap The value of the new Exposure Cap - */ - function _updateExposureCap(uint128 exposureCap) internal { - uint128 oldExposureCap = _exposureCap; - _exposureCap = exposureCap; - emit ExposureCapUpdated(oldExposureCap, exposureCap); - } - - /** - * @dev Updates GHO Treasury Address - * @param newGhoTreasury The address of the new GHO Treasury - */ - function _updateGhoTreasury(address newGhoTreasury) internal { - require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); - address oldGhoTreasury = _ghoTreasury; - _ghoTreasury = newGhoTreasury; - emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); - } - - /// @inheritdoc VersionedInitializable - function getRevision() internal pure virtual override returns (uint256) { - return GSM_REVISION(); - } -} From 3d7cbb0d0efe090ce69c53aa4b9dec040bfea2d9 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 24 Sep 2024 17:31:21 -0500 Subject: [PATCH 60/68] feat: init GsmConverter overview doc --- docs/buidl-gsm-converter.md | 21 +++++++++++++++++++++ docs/img/buyAsset.png | Bin 0 -> 54993 bytes 2 files changed, 21 insertions(+) create mode 100644 docs/buidl-gsm-converter.md create mode 100644 docs/img/buyAsset.png diff --git a/docs/buidl-gsm-converter.md b/docs/buidl-gsm-converter.md new file mode 100644 index 00000000..088ae377 --- /dev/null +++ b/docs/buidl-gsm-converter.md @@ -0,0 +1,21 @@ +# BUIDL GSM Converter + +## Overview + +The BUIDL GSM Converter is designed to allow for integration of the GSM ([GHO Stability Module](https://docs.gho.xyz/developer-docs/gho-stability-module)) with [Blackrock BUIDL fund](https://securitize.io/learn/press/blackrock-launches-first-tokenized-fund-buidl-on-the-ethereum-network) subscriptions and redemptions, adhering to the [BUIDL GSM Temp Check](https://governance.aave.com/t/temp-check-buidl-gsm/18775). + +The combined BUIDL GSM instance with GsmConverter will enable 1:1 fixed-ratio swaps between USDC and GHO, utilizing the USDC surplus to mint BUIDL tokens. The converter architecture as an intermediary allows for minimal required code changes to the GSM, while also providing a standardized smart contract interface for users. Furthermore, BUIDL holders will be able to interact directly with the BUIDL GSM as they do with other existing GSMs. + +### BUIDL GSM ([Gsm.sol](../src/contracts/facilitators/gsm/Gsm.sol)) + +The BUIDL GSM operates like other existing GSM instances, except it specifically facilitates conversions between GHO and BUIDL. BUIDL holders can interact with this contract directly. + +### GSM Converter ([GsmConverter.sol](../src/contracts/facilitators/gsm/converter/GsmConverter.sol)) + +The GSM Converter will act as a middleware between the user and BUIDL GSM, facilitating conversions between USDC and BUIDL under the hood. + +During a `buyAsset` transaction, where users sell GHO to receive USDC, the GsmConverter first interacts with the BUIDL GSM to convert GHO to BUIDL. Then it integrates with the [Circle BUIDL Off-Ramp contract](https://etherscan.io/address/0x31d3f59ad4aac0eee2247c65ebe8bf6e9e470a53#code) in order to redeem USDC in exchange for BUIDL. This USDC is then returned to the user. + +![buyAsset](./img/buyAsset.png) + +During a `sellAsset` transaction, where users sell USDC to receive GHO, ... diff --git a/docs/img/buyAsset.png b/docs/img/buyAsset.png new file mode 100644 index 0000000000000000000000000000000000000000..4b77c595c9b9d40f8229261b44e9f53cf0ef9b61 GIT binary patch literal 54993 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 Date: Wed, 25 Sep 2024 10:23:14 -0500 Subject: [PATCH 61/68] test: reduce unneeded mock code for MockGsmFailedSellAssetRemainingGhoBalance --- src/test/TestGsmConverter.t.sol | 3 + ...kGsmFailedSellAssetRemainingGhoBalance.sol | 185 ++---------------- 2 files changed, 20 insertions(+), 168 deletions(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 710870c2..7325db9d 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -1188,6 +1188,9 @@ contract TestGsmConverter is TestGhoBase { vm.stopPrank(); } + // TODO: testRevertSellAssetWithSigInvalidRemainingGhoBalance + // _upgradeToGsmFailedSellAssetRemainingGhoBalance + function testBuyAsset() public { (uint256 expectedRedeemedAssetAmount, uint256 expectedGhoSold, , uint256 buyFee) = GHO_BUIDL_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_BUIDL_AMOUNT); diff --git a/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol index 81831e4e..b87d5ba0 100644 --- a/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol +++ b/src/test/mocks/MockGsmFailedSellAssetRemainingGhoBalance.sol @@ -134,7 +134,8 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is uint256 minAmount, address receiver ) external notFrozen notSeized returns (uint256, uint256) { - return _buyAsset(msg.sender, minAmount, receiver); + // return default values + return (0, 0); } /// @inheritdoc IGsm @@ -145,21 +146,8 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is uint256 deadline, bytes calldata signature ) external notFrozen notSeized returns (uint256, uint256) { - require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); - bytes32 digest = keccak256( - abi.encode( - '\x19\x01', - _domainSeparatorV4(), - BUY_ASSET_WITH_SIG_TYPEHASH, - abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) - ) - ); - require( - SignatureChecker.isValidSignatureNow(originator, digest, signature), - 'SIGNATURE_INVALID' - ); - - return _buyAsset(originator, minAmount, receiver); + // return default values + return (0, 0); } /// @inheritdoc IGsm @@ -201,85 +189,43 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is address to, uint256 amount ) external onlyRole(TOKEN_RESCUER_ROLE) { - require(amount > 0, 'INVALID_AMOUNT'); - if (token == GHO_TOKEN) { - uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; - require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); - } - if (token == UNDERLYING_ASSET) { - uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; - require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); - } - IERC20(token).safeTransfer(to, amount); - emit TokensRescued(token, to, amount); + // intentionally left blank } /// @inheritdoc IGsm function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { - if (enable) { - require(!_isFrozen, 'GSM_ALREADY_FROZEN'); - } else { - require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); - } - _isFrozen = enable; - emit SwapFreeze(msg.sender, enable); + // intentionally left blank } /// @inheritdoc IGsm function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { - _isSeized = true; - _currentExposure = 0; - _updateExposureCap(0); - - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); - if (underlyingBalance > 0) { - IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); - } - - emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); - return underlyingBalance; + return 0; } /// @inheritdoc IGsm function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { - require(_isSeized, 'GSM_NOT_SEIZED'); - require(amount > 0, 'INVALID_AMOUNT'); - - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - if (amount > ghoMinted) { - amount = ghoMinted; - } - IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); - IGhoToken(GHO_TOKEN).burn(amount); - - emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); - return amount; + // return default values + return 0; } /// @inheritdoc IGsm function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { - _updateFeeStrategy(feeStrategy); + // intentionally left blank } /// @inheritdoc IGsm function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { - _updateExposureCap(exposureCap); + // intentionally left blank } /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() public virtual override { - uint256 accruedFees = _accruedFees; - if (accruedFees > 0) { - _accruedFees = 0; - IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); - emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); - } + // intentionally left blank } /// @inheritdoc IGhoFacilitator function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { - _updateGhoTreasury(newGhoTreasury); + // intentionally left blank } /// @inheritdoc IGsm @@ -291,13 +237,8 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is function getGhoAmountForBuyAsset( uint256 minAssetAmount ) external view returns (uint256, uint256, uint256, uint256) { - ( - uint256 assetAmount, - uint256 ghoSold, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForBuyAsset(minAssetAmount); - return (assetAmount, ghoSold, grossAmount, fee); + // return default values + return (0, 0, 0, 0); } /// @inheritdoc IGsm @@ -311,18 +252,8 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is function getAssetAmountForBuyAsset( uint256 maxGhoAmount ) external view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - uint256 grossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) - : maxGhoAmount; - // round down so maxGhoAmount is guaranteed - uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); - uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( - assetAmount, - true - ); - uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; - return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); + // return default values + return (0, 0, 0, 0); } /// @inheritdoc IGsm @@ -393,50 +324,6 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is return 2; } - /** - * @dev Buys an underlying asset with GHO - * @param originator The originator of the request - * @param minAmount The minimum amount of the underlying asset desired for purchase - * @param receiver The recipient address of the underlying asset being purchased - * @return The amount of underlying asset bought - * @return The amount of GHO sold by the user - */ - function _buyAsset( - address originator, - uint256 minAmount, - address receiver - ) internal returns (uint256, uint256) { - ( - uint256 assetAmount, - uint256 ghoSold, - uint256 grossAmount, - uint256 fee - ) = _calculateGhoAmountForBuyAsset(minAmount); - - _beforeBuyAsset(originator, assetAmount, receiver); - - require(assetAmount > 0, 'INVALID_AMOUNT'); - require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); - - _currentExposure -= uint128(assetAmount); - _accruedFees += fee.toUint128(); - IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); - IGhoToken(GHO_TOKEN).burn(grossAmount); - IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); - - emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); - return (assetAmount, ghoSold); - } - - /** - * @dev Hook that is called before `buyAsset`. - * @dev This can be used to add custom logic - * @param originator Originator of the request - * @param amount The amount of the underlying asset desired for purchase - * @param receiver Recipient address of the underlying asset being purchased - */ - function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} - /** * @dev Sells an underlying asset for GHO * @param originator The originator of the request @@ -488,34 +375,6 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is address receiver ) internal virtual {} - /** - * @dev Returns the amount of GHO sold in exchange of buying underlying asset - * @param assetAmount The amount of underlying asset to buy - * @return The exact amount of asset the user purchases - * @return The total amount of GHO the user sells (gross amount in GHO plus fee) - * @return The gross amount of GHO - * @return The fee amount in GHO, applied on top of gross amount of GHO - */ - function _calculateGhoAmountForBuyAsset( - uint256 assetAmount - ) internal view returns (uint256, uint256, uint256, uint256) { - bool withFee = _feeStrategy != address(0); - // pick the highest GHO amount possible for given asset amount - uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); - uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; - uint256 ghoSold = grossAmount + fee; - uint256 finalGrossAmount = withFee - ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) - : ghoSold; - // pick the lowest asset amount possible for given GHO amount - uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( - finalGrossAmount, - false - ); - uint256 finalFee = ghoSold - finalGrossAmount; - return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); - } - /** * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset * @param assetAmount The amount of underlying asset to sell @@ -544,16 +403,6 @@ contract MockGsmFailedSellAssetRemainingGhoBalance is return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); } - /** - * @dev Updates Fee Strategy - * @param feeStrategy The address of the new Fee Strategy - */ - function _updateFeeStrategy(address feeStrategy) internal { - address oldFeeStrategy = _feeStrategy; - _feeStrategy = feeStrategy; - emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); - } - /** * @dev Updates Exposure Cap * @param exposureCap The value of the new Exposure Cap From 53dc395969e97e12634e37278d29952599507f34 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 14 Oct 2024 10:41:18 -0500 Subject: [PATCH 62/68] refactor: rename subscription event --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 2 +- .../facilitators/gsm/converter/interfaces/IGsmConverter.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 4e11f57a..f7456c5b 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -257,7 +257,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' ); - emit SellAssetThroughIssuance(originator, receiver, redeemedAssetAmount, ghoBought); + emit SellAssetThroughSubscription(originator, receiver, redeemedAssetAmount, ghoBought); return (assetAmount, ghoBought); } } diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index d9c4e104..0639ac6b 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -23,13 +23,13 @@ interface IGsmConverter { ); /** - * @dev Emitted when a user sells an asset (buying GHO) in the GSM after an issuance + * @dev Emitted when a user sells an asset (buying GHO) in the GSM after an asset subscription * @param originator The address of the seller originating the request * @param receiver The address of the receiver of GHO * @param redeemedAssetAmount The amount of the redeemed asset converted * @param ghoAmount The amount of GHO bought, inclusive of fee */ - event SellAssetThroughIssuance( + event SellAssetThroughSubscription( address indexed originator, address indexed receiver, uint256 redeemedAssetAmount, From b48a0be3d955533215d34bf72f6da394f3ee639c Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 14 Oct 2024 10:48:52 -0500 Subject: [PATCH 63/68] test: resolve tests from event change, rename mock files to be BUIDL-specific --- .../gsm/converter/GsmConverter.sol | 4 ++-- src/test/TestGhoBase.t.sol | 18 +++++++++--------- src/test/TestGsmConverter.t.sol | 16 ++++++++-------- src/test/helpers/Events.sol | 2 +- ...eiver.sol => MockBUIDLIssuanceReceiver.sol} | 4 ++-- ...sol => MockBUIDLIssuanceReceiverFailed.sol} | 4 ++-- ...uanceReceiverFailedInvalidUSDCAccepted.sol} | 4 ++-- 7 files changed, 26 insertions(+), 26 deletions(-) rename src/test/mocks/{MockIssuanceReceiver.sol => MockBUIDLIssuanceReceiver.sol} (92%) rename src/test/mocks/{MockIssuanceReceiverFailed.sol => MockBUIDLIssuanceReceiverFailed.sol} (91%) rename src/test/mocks/{MockIssuanceReceiverFailedInvalidUSDCAccepted.sol => MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol} (89%) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index f7456c5b..abff9847 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -11,7 +11,7 @@ import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; // TODO: replace with proper issuance implementation later -import {MockIssuanceReceiver} from '../../../../test/mocks/MockIssuanceReceiver.sol'; +import {MockBUIDLIssuanceReceiver} from '../../../../test/mocks/MockBUIDLIssuanceReceiver.sol'; import 'forge-std/console2.sol'; @@ -234,7 +234,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, redeemedAssetAmount); //TODO: replace with proper issuance implementation later - MockIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); + MockBUIDLIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); require( IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialissuedAssetBalance + redeemedAssetAmount, diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 354a7451..8777a6f4 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -34,9 +34,9 @@ import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; import {MockRedemptionFailedIssuedAssetAmount} from './mocks/MockRedemptionFailedIssuedAssetAmount.sol'; import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; -import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; -import {MockIssuanceReceiverFailed} from './mocks/MockIssuanceReceiverFailed.sol'; -import {MockIssuanceReceiverFailedInvalidUSDCAccepted} from './mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol'; +import {MockBUIDLIssuanceReceiver} from './mocks/MockBUIDLIssuanceReceiver.sol'; +import {MockBUIDLIssuanceReceiverFailed} from './mocks/MockBUIDLIssuanceReceiverFailed.sol'; +import {MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted} from './mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol'; import {MockPoolDataProvider} from './mocks/MockPoolDataProvider.sol'; // interfaces @@ -128,9 +128,9 @@ contract TestGhoBase is Test, Constants, Events { MockRedemption BUIDL_USDC_REDEMPTION; MockRedemptionFailedIssuedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT; MockRedemptionFailed BUIDL_USDC_REDEMPTION_FAILED; - MockIssuanceReceiver BUIDL_USDC_ISSUANCE; - MockIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; - MockIssuanceReceiverFailedInvalidUSDCAccepted BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC; + MockBUIDLIssuanceReceiver BUIDL_USDC_ISSUANCE; + MockBUIDLIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; + MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -408,12 +408,12 @@ contract TestGhoBase is Test, Constants, Events { address(BUIDL_TOKEN), address(USDC_TOKEN) ); - BUIDL_USDC_ISSUANCE = new MockIssuanceReceiver(address(BUIDL_TOKEN), address(USDC_TOKEN)); - BUIDL_USDC_ISSUANCE_FAILED = new MockIssuanceReceiverFailed( + BUIDL_USDC_ISSUANCE = new MockBUIDLIssuanceReceiver(address(BUIDL_TOKEN), address(USDC_TOKEN)); + BUIDL_USDC_ISSUANCE_FAILED = new MockBUIDLIssuanceReceiverFailed( address(BUIDL_TOKEN), address(USDC_TOKEN) ); - BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC = new MockIssuanceReceiverFailedInvalidUSDCAccepted( + BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC = new MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted( address(BUIDL_TOKEN), address(USDC_TOKEN) ); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index 7325db9d..bcd0b036 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -122,7 +122,7 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + emit SellAssetThroughSubscription(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, ALICE @@ -200,7 +200,7 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, BOB, expectedIssuedAssetAmount, expectedGhoBought); + emit SellAssetThroughSubscription(ALICE, BOB, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, BOB @@ -288,7 +288,7 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + emit SellAssetThroughSubscription(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, ALICE @@ -367,7 +367,7 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + emit SellAssetThroughSubscription(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset(maxAmount, ALICE); vm.stopPrank(); @@ -452,7 +452,7 @@ contract TestGsmConverter is TestGhoBase { USDC_TOKEN.approve(address(GSM_CONVERTER), expectedIssuedAssetAmount); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); + emit SellAssetThroughSubscription(ALICE, ALICE, expectedIssuedAssetAmount, expectedGhoBought); (uint256 assetAmount, uint256 ghoBought) = GSM_CONVERTER.sellAsset( DEFAULT_GSM_BUIDL_AMOUNT, ALICE @@ -633,7 +633,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(ALICE); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance( + emit SellAssetThroughSubscription( gsmConverterSignerAddr, gsmConverterSignerAddr, expectedIssuedAssetAmount, @@ -760,7 +760,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(ALICE); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance( + emit SellAssetThroughSubscription( gsmConverterSignerAddr, gsmConverterSignerAddr, expectedIssuedAssetAmount, @@ -892,7 +892,7 @@ contract TestGsmConverter is TestGhoBase { vm.prank(ALICE); vm.expectEmit(true, true, true, true, address(GSM_CONVERTER)); - emit SellAssetThroughIssuance( + emit SellAssetThroughSubscription( gsmConverterSignerAddr, gsmConverterSignerAddr, expectedIssuedAssetAmount, diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index 1fd32968..c6e58759 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -118,7 +118,7 @@ interface Events { uint256 issuedAssetAmount, uint256 ghoAmount ); - event SellAssetThroughIssuance( + event SellAssetThroughSubscription( address indexed originator, address indexed receiver, uint256 redeemedAssetAmount, diff --git a/src/test/mocks/MockIssuanceReceiver.sol b/src/test/mocks/MockBUIDLIssuanceReceiver.sol similarity index 92% rename from src/test/mocks/MockIssuanceReceiver.sol rename to src/test/mocks/MockBUIDLIssuanceReceiver.sol index e82452ba..e67c1630 100644 --- a/src/test/mocks/MockIssuanceReceiver.sol +++ b/src/test/mocks/MockBUIDLIssuanceReceiver.sol @@ -4,9 +4,9 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockIssuanceReceiver + * @title MockBUIDLIssuanceReceiver */ -contract MockIssuanceReceiver { +contract MockBUIDLIssuanceReceiver { using SafeERC20 for IERC20; address public immutable asset; diff --git a/src/test/mocks/MockIssuanceReceiverFailed.sol b/src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol similarity index 91% rename from src/test/mocks/MockIssuanceReceiverFailed.sol rename to src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol index d85376fe..b8c1dbce 100644 --- a/src/test/mocks/MockIssuanceReceiverFailed.sol +++ b/src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol @@ -4,9 +4,9 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockIssuanceReceiverFailed + * @title MockBUIDLIssuanceReceiverFailed */ -contract MockIssuanceReceiverFailed { +contract MockBUIDLIssuanceReceiverFailed { using SafeERC20 for IERC20; address public immutable asset; diff --git a/src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol b/src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol similarity index 89% rename from src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol rename to src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol index 2dad5c5a..a7a7e88e 100644 --- a/src/test/mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol +++ b/src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol @@ -4,10 +4,10 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockIssuanceReceiverFailedInvalidUSDCAccepted + * @title MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted * @dev During issuance, the contract does not accept the proper amount of USDC but issues asset properly */ -contract MockIssuanceReceiverFailedInvalidUSDCAccepted { +contract MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted { using SafeERC20 for IERC20; address public immutable asset; From 72ee91dd01f2ec3864545c8cbdbd1ff9ff48eb7b Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 14 Oct 2024 10:58:56 -0500 Subject: [PATCH 64/68] refactor: rename issuance to subscription --- .../gsm/converter/GsmConverter.sol | 12 ++++++------ .../gsm/converter/interfaces/IGsmConverter.sol | 6 +++--- src/test/TestGhoBase.t.sol | 18 +++++++++--------- src/test/TestGsmConverter.t.sol | 2 +- ...eReceiver.sol => MockBUIDLSubscription.sol} | 4 ++-- ...led.sol => MockBUIDLSubscriptionFailed.sol} | 4 ++-- ...LSubscriptionFailedInvalidUSDCAccepted.sol} | 4 ++-- 7 files changed, 25 insertions(+), 25 deletions(-) rename src/test/mocks/{MockBUIDLIssuanceReceiver.sol => MockBUIDLSubscription.sol} (92%) rename src/test/mocks/{MockBUIDLIssuanceReceiverFailed.sol => MockBUIDLSubscriptionFailed.sol} (91%) rename src/test/mocks/{MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol => MockBUIDLSubscriptionFailedInvalidUSDCAccepted.sol} (89%) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index abff9847..065394c5 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -11,7 +11,7 @@ import {IGsm} from '../interfaces/IGsm.sol'; import {IGsmConverter} from './interfaces/IGsmConverter.sol'; import {IRedemption} from '../dependencies/circle/IRedemption.sol'; // TODO: replace with proper issuance implementation later -import {MockBUIDLIssuanceReceiver} from '../../../../test/mocks/MockBUIDLIssuanceReceiver.sol'; +import {MockBUIDLSubscription} from '../../../../test/mocks/MockBUIDLSubscription.sol'; import 'forge-std/console2.sol'; @@ -51,7 +51,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address public immutable REDEMPTION_CONTRACT; /// @inheritdoc IGsmConverter - address public immutable ISSUANCE_RECEIVER_CONTRACT; + address public immutable SUBSCRIPTION_CONTRACT; /// @inheritdoc IGsmConverter mapping(address => uint256) public nonces; @@ -81,7 +81,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { GSM = gsm; REDEMPTION_CONTRACT = redemptionContract; - ISSUANCE_RECEIVER_CONTRACT = issuanceReceiverContract; + SUBSCRIPTION_CONTRACT = issuanceReceiverContract; ISSUED_ASSET = issuedAsset; // BUIDL REDEEMED_ASSET = redeemedAsset; // USDC GHO_TOKEN = IGsm(GSM).GHO_TOKEN(); @@ -232,16 +232,16 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { (uint256 redeemedAssetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); - IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, redeemedAssetAmount); + IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, redeemedAssetAmount); //TODO: replace with proper issuance implementation later - MockBUIDLIssuanceReceiver(ISSUANCE_RECEIVER_CONTRACT).issuance(redeemedAssetAmount); + MockBUIDLSubscription(SUBSCRIPTION_CONTRACT).issuance(redeemedAssetAmount); require( IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialissuedAssetBalance + redeemedAssetAmount, 'INVALID_ISSUANCE' ); // reset approval after issuance - IERC20(REDEEMED_ASSET).approve(ISSUANCE_RECEIVER_CONTRACT, 0); + IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, 0); IERC20(ISSUED_ASSET).approve(GSM, redeemedAssetAmount); (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 0639ac6b..7f2c7ee0 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -143,10 +143,10 @@ interface IGsmConverter { function REDEMPTION_CONTRACT() external view returns (address); /** - * @notice Returns the address of the issuance receiver contract that manages asset issuance - * @return The address of the issuance receiver contract + * @notice Returns the address of the subscription contract that manages asset issuance + * @return The address of the subscription contract */ - function ISSUANCE_RECEIVER_CONTRACT() external view returns (address); + function SUBSCRIPTION_CONTRACT() external view returns (address); /** * @notice Returns the current nonce (for EIP-712 signature methods) of an address diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 8777a6f4..ed376b04 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -34,9 +34,9 @@ import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockRedemption} from './mocks/MockRedemption.sol'; import {MockRedemptionFailedIssuedAssetAmount} from './mocks/MockRedemptionFailedIssuedAssetAmount.sol'; import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; -import {MockBUIDLIssuanceReceiver} from './mocks/MockBUIDLIssuanceReceiver.sol'; -import {MockBUIDLIssuanceReceiverFailed} from './mocks/MockBUIDLIssuanceReceiverFailed.sol'; -import {MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted} from './mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol'; +import {MockBUIDLSubscription} from './mocks/MockBUIDLSubscription.sol'; +import {MockBUIDLSubscriptionFailed} from './mocks/MockBUIDLSubscriptionFailed.sol'; +import {MockBUIDLSubscriptionFailedInvalidUSDCAccepted} from './mocks/MockBUIDLSubscriptionFailedInvalidUSDCAccepted.sol'; import {MockPoolDataProvider} from './mocks/MockPoolDataProvider.sol'; // interfaces @@ -128,9 +128,9 @@ contract TestGhoBase is Test, Constants, Events { MockRedemption BUIDL_USDC_REDEMPTION; MockRedemptionFailedIssuedAssetAmount BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT; MockRedemptionFailed BUIDL_USDC_REDEMPTION_FAILED; - MockBUIDLIssuanceReceiver BUIDL_USDC_ISSUANCE; - MockBUIDLIssuanceReceiverFailed BUIDL_USDC_ISSUANCE_FAILED; - MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC; + MockBUIDLSubscription BUIDL_USDC_ISSUANCE; + MockBUIDLSubscriptionFailed BUIDL_USDC_ISSUANCE_FAILED; + MockBUIDLSubscriptionFailedInvalidUSDCAccepted BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; @@ -408,12 +408,12 @@ contract TestGhoBase is Test, Constants, Events { address(BUIDL_TOKEN), address(USDC_TOKEN) ); - BUIDL_USDC_ISSUANCE = new MockBUIDLIssuanceReceiver(address(BUIDL_TOKEN), address(USDC_TOKEN)); - BUIDL_USDC_ISSUANCE_FAILED = new MockBUIDLIssuanceReceiverFailed( + BUIDL_USDC_ISSUANCE = new MockBUIDLSubscription(address(BUIDL_TOKEN), address(USDC_TOKEN)); + BUIDL_USDC_ISSUANCE_FAILED = new MockBUIDLSubscriptionFailed( address(BUIDL_TOKEN), address(USDC_TOKEN) ); - BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC = new MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted( + BUIDL_USDC_ISSUANCE_FAILED_INVALID_USDC = new MockBUIDLSubscriptionFailedInvalidUSDCAccepted( address(BUIDL_TOKEN), address(USDC_TOKEN) ); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index bcd0b036..b594e640 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -29,7 +29,7 @@ contract TestGsmConverter is TestGhoBase { 'Unexpected redemption contract address' ); assertEq( - gsmConverter.ISSUANCE_RECEIVER_CONTRACT(), + gsmConverter.SUBSCRIPTION_CONTRACT(), address(BUIDL_USDC_ISSUANCE), 'Unexpected issuance receiver contract address' ); diff --git a/src/test/mocks/MockBUIDLIssuanceReceiver.sol b/src/test/mocks/MockBUIDLSubscription.sol similarity index 92% rename from src/test/mocks/MockBUIDLIssuanceReceiver.sol rename to src/test/mocks/MockBUIDLSubscription.sol index e67c1630..b6e16f96 100644 --- a/src/test/mocks/MockBUIDLIssuanceReceiver.sol +++ b/src/test/mocks/MockBUIDLSubscription.sol @@ -4,9 +4,9 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockBUIDLIssuanceReceiver + * @title MockBUIDLSubscription */ -contract MockBUIDLIssuanceReceiver { +contract MockBUIDLSubscription { using SafeERC20 for IERC20; address public immutable asset; diff --git a/src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol b/src/test/mocks/MockBUIDLSubscriptionFailed.sol similarity index 91% rename from src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol rename to src/test/mocks/MockBUIDLSubscriptionFailed.sol index b8c1dbce..a73f1ab5 100644 --- a/src/test/mocks/MockBUIDLIssuanceReceiverFailed.sol +++ b/src/test/mocks/MockBUIDLSubscriptionFailed.sol @@ -4,9 +4,9 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockBUIDLIssuanceReceiverFailed + * @title MockBUIDLSubscriptionFailed */ -contract MockBUIDLIssuanceReceiverFailed { +contract MockBUIDLSubscriptionFailed { using SafeERC20 for IERC20; address public immutable asset; diff --git a/src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol b/src/test/mocks/MockBUIDLSubscriptionFailedInvalidUSDCAccepted.sol similarity index 89% rename from src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol rename to src/test/mocks/MockBUIDLSubscriptionFailedInvalidUSDCAccepted.sol index a7a7e88e..942aadcf 100644 --- a/src/test/mocks/MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted.sol +++ b/src/test/mocks/MockBUIDLSubscriptionFailedInvalidUSDCAccepted.sol @@ -4,10 +4,10 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; /** - * @title MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted + * @title MockBUIDLSubscriptionFailedInvalidUSDCAccepted * @dev During issuance, the contract does not accept the proper amount of USDC but issues asset properly */ -contract MockBUIDLIssuanceReceiverFailedInvalidUSDCAccepted { +contract MockBUIDLSubscriptionFailedInvalidUSDCAccepted { using SafeERC20 for IERC20; address public immutable asset; From 16a5d597a631468d52f14ef3e44128cd0414fc73 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 14 Oct 2024 11:32:32 -0500 Subject: [PATCH 65/68] feat: add todo notes on subscription fees --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index 065394c5..b74a90a8 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -235,6 +235,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, redeemedAssetAmount); //TODO: replace with proper issuance implementation later MockBUIDLSubscription(SUBSCRIPTION_CONTRACT).issuance(redeemedAssetAmount); + // TODO: probably will be fees from issuance, so need to adjust the logic require( IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialissuedAssetBalance + redeemedAssetAmount, @@ -243,6 +244,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { // reset approval after issuance IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, 0); + // TODO: account for fees for sellAsset amount param IERC20(ISSUED_ASSET).approve(GSM, redeemedAssetAmount); (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); // reset approval after sellAsset From c943e73b28128aa4f84329e86a6188675659cb38 Mon Sep 17 00:00:00 2001 From: YBM Date: Mon, 14 Oct 2024 13:10:07 -0500 Subject: [PATCH 66/68] fix: account for subscription fee, resolve tests, param names --- .../gsm/converter/GsmConverter.sol | 48 ++++++++++++------- .../converter/interfaces/IGsmConverter.sol | 8 ++-- src/test/TestGsmConverter.t.sol | 2 +- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index b74a90a8..a170eec2 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -185,20 +185,20 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); - (uint256 issuedAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); + (uint256 boughtAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); IGhoToken(GHO_TOKEN).approve(address(GSM), 0); - IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), issuedAssetAmount); - IRedemption(REDEMPTION_CONTRACT).redeem(issuedAssetAmount); - // redeemedAssetAmount matches issuedAssetAmount because Redemption exchanges in 1:1 ratio + IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), boughtAssetAmount); + IRedemption(REDEMPTION_CONTRACT).redeem(boughtAssetAmount); + // Redemption exchanges in 1:1 ratio between BUIDL and USDC require( IERC20(REDEEMED_ASSET).balanceOf(address(this)) == - initialRedeemedAssetBalance + issuedAssetAmount, + initialRedeemedAssetBalance + boughtAssetAmount, 'INVALID_REDEMPTION' ); IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), 0); - IERC20(REDEEMED_ASSET).safeTransfer(receiver, issuedAssetAmount); + IERC20(REDEEMED_ASSET).safeTransfer(receiver, boughtAssetAmount); require( IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, @@ -209,8 +209,8 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { 'INVALID_REMAINING_ISSUED_ASSET_BALANCE' ); - emit BuyAssetThroughRedemption(originator, receiver, issuedAssetAmount, ghoSold); - return (issuedAssetAmount, ghoSold); + emit BuyAssetThroughRedemption(originator, receiver, boughtAssetAmount, ghoSold); + return (boughtAssetAmount, ghoSold); } /** @@ -227,29 +227,37 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { address receiver ) internal returns (uint256, uint256) { uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this)); - uint256 initialissuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); + uint256 initialIssuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this)); uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); - (uint256 redeemedAssetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); - IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), redeemedAssetAmount); - IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, redeemedAssetAmount); + (uint256 assetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); // asset is BUIDL + IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), assetAmount); + IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, assetAmount); //TODO: replace with proper issuance implementation later - MockBUIDLSubscription(SUBSCRIPTION_CONTRACT).issuance(redeemedAssetAmount); + MockBUIDLSubscription(SUBSCRIPTION_CONTRACT).issuance(assetAmount); + uint256 subscribedAssetAmount = IERC20(ISSUED_ASSET).balanceOf(address(this)) - + initialIssuedAssetBalance; // TODO: probably will be fees from issuance, so need to adjust the logic + // only use this require only if preview of issuance is possible, otherwise it is redundant require( IERC20(ISSUED_ASSET).balanceOf(address(this)) == - initialissuedAssetBalance + redeemedAssetAmount, + initialIssuedAssetBalance + subscribedAssetAmount, 'INVALID_ISSUANCE' ); // reset approval after issuance IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, 0); // TODO: account for fees for sellAsset amount param - IERC20(ISSUED_ASSET).approve(GSM, redeemedAssetAmount); - (uint256 assetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset(maxAmount, receiver); + (assetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(subscribedAssetAmount); // recalculate based on actual issuance amount, < maxAmount + IERC20(ISSUED_ASSET).approve(GSM, assetAmount); + (uint256 soldAssetAmount, uint256 ghoBought) = IGsm(GSM).sellAsset( + subscribedAssetAmount, + receiver + ); // reset approval after sellAsset IERC20(ISSUED_ASSET).approve(GSM, 0); + // by the end of the transaction, this contract should not retain any of the assets require( IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance, 'INVALID_REMAINING_GHO_BALANCE' @@ -258,8 +266,12 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { IERC20(REDEEMED_ASSET).balanceOf(address(this)) == initialRedeemedAssetBalance, 'INVALID_REMAINING_REDEEMED_ASSET_BALANCE' ); + require( + IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialIssuedAssetBalance, + 'INVALID_REMAINING_ISSUED_ASSET_BALANCE' + ); - emit SellAssetThroughSubscription(originator, receiver, redeemedAssetAmount, ghoBought); - return (assetAmount, ghoBought); + emit SellAssetThroughSubscription(originator, receiver, soldAssetAmount, ghoBought); + return (soldAssetAmount, ghoBought); } } diff --git a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol index 7f2c7ee0..ebc7580d 100644 --- a/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/interfaces/IGsmConverter.sol @@ -12,13 +12,13 @@ interface IGsmConverter { * @dev Emitted when a user buys an asset (selling GHO) in the GSM after a redemption * @param originator The address of the buyer originating the request * @param receiver The address of the receiver of the underlying asset - * @param issuedAssetAmount The amount of the issued asset converted + * @param boughtAssetAmount The amount of the asset bought * @param ghoAmount The amount of total GHO sold, inclusive of fee */ event BuyAssetThroughRedemption( address indexed originator, address indexed receiver, - uint256 issuedAssetAmount, + uint256 boughtAssetAmount, uint256 ghoAmount ); @@ -26,13 +26,13 @@ interface IGsmConverter { * @dev Emitted when a user sells an asset (buying GHO) in the GSM after an asset subscription * @param originator The address of the seller originating the request * @param receiver The address of the receiver of GHO - * @param redeemedAssetAmount The amount of the redeemed asset converted + * @param soldAssetAmount The amount of the asset sold * @param ghoAmount The amount of GHO bought, inclusive of fee */ event SellAssetThroughSubscription( address indexed originator, address indexed receiver, - uint256 redeemedAssetAmount, + uint256 soldAssetAmount, uint256 ghoAmount ); diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index b594e640..e2de64ff 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -549,7 +549,7 @@ contract TestGsmConverter is TestGhoBase { vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsmConverter), DEFAULT_GSM_BUIDL_AMOUNT); - vm.expectRevert('INVALID_ISSUANCE'); + vm.expectRevert('INVALID_AMOUNT'); gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); } From 0201eea2dabc908e4e2ff9a8258716b56d42c609 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 15 Oct 2024 08:13:50 -0500 Subject: [PATCH 67/68] test: end prank --- src/test/TestGsmConverter.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/TestGsmConverter.t.sol b/src/test/TestGsmConverter.t.sol index e2de64ff..2e7de5df 100644 --- a/src/test/TestGsmConverter.t.sol +++ b/src/test/TestGsmConverter.t.sol @@ -544,13 +544,14 @@ contract TestGsmConverter is TestGhoBase { address(USDC_TOKEN) ); - vm.prank(FAUCET); + vm.startPrank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_BUIDL_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsmConverter), DEFAULT_GSM_BUIDL_AMOUNT); vm.expectRevert('INVALID_AMOUNT'); gsmConverter.sellAsset(DEFAULT_GSM_BUIDL_AMOUNT, ALICE); + vm.stopPrank(); } function testRevertSellAssetInvalidRemainingGhoBalance() public { From b11bfc62b5cf638ade087b43118ea7f9416c7ef5 Mon Sep 17 00:00:00 2001 From: YBM Date: Tue, 15 Oct 2024 08:41:39 -0500 Subject: [PATCH 68/68] fix: safe operations --- src/contracts/facilitators/gsm/converter/GsmConverter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/facilitators/gsm/converter/GsmConverter.sol b/src/contracts/facilitators/gsm/converter/GsmConverter.sol index a170eec2..7f3e67a6 100644 --- a/src/contracts/facilitators/gsm/converter/GsmConverter.sol +++ b/src/contracts/facilitators/gsm/converter/GsmConverter.sol @@ -183,7 +183,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { (, uint256 ghoAmount, , ) = IGsm(GSM).getGhoAmountForBuyAsset(minAmount); - IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoAmount); + IERC20(GHO_TOKEN).safeTransferFrom(originator, address(this), ghoAmount); IGhoToken(GHO_TOKEN).approve(address(GSM), ghoAmount); (uint256 boughtAssetAmount, uint256 ghoSold) = IGsm(GSM).buyAsset(minAmount, address(this)); require(ghoAmount == ghoSold, 'INVALID_GHO_SOLD'); @@ -231,7 +231,7 @@ contract GsmConverter is Ownable, EIP712, IGsmConverter { uint256 initialRedeemedAssetBalance = IERC20(REDEEMED_ASSET).balanceOf(address(this)); (uint256 assetAmount, , , ) = IGsm(GSM).getGhoAmountForSellAsset(maxAmount); // asset is BUIDL - IERC20(REDEEMED_ASSET).transferFrom(originator, address(this), assetAmount); + IERC20(REDEEMED_ASSET).safeTransferFrom(originator, address(this), assetAmount); IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, assetAmount); //TODO: replace with proper issuance implementation later MockBUIDLSubscription(SUBSCRIPTION_CONTRACT).issuance(assetAmount);