Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

USTB GSM Implementation #431

Closed
wants to merge 72 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
ab52741
feat: skeleton for GsmConverter buyAsset
yan-man Aug 30, 2024
b102136
fix: resolve compilation issues
yan-man Sep 4, 2024
dd74cc2
feat: initialize interface
yan-man Sep 4, 2024
023a6b4
refactor: move files to separate directory
yan-man Sep 4, 2024
750eb61
fix: resolve interface function visibility
yan-man Sep 4, 2024
038e3be
test: initialize constructor unit test
yan-man Sep 4, 2024
c574b35
test: testRevertConstructorZeroAddressParams;
yan-man Sep 4, 2024
56ab408
test: initialize BUIDL-specific test GSM
yan-man Sep 5, 2024
5760379
test: add check on buyAsset for minAmount
yan-man Sep 5, 2024
bb08c59
test: happy path for converter buyAsset
yan-man Sep 5, 2024
eb4a888
test: events testing + happy path buyAsset
yan-man Sep 5, 2024
8ca6481
test: test revert cases
yan-man Sep 5, 2024
2942b36
fix: resolve tests involving facilitator due to new one added
yan-man Sep 5, 2024
ebca6d7
fix: resolve fuzz test input with zero address
yan-man Sep 5, 2024
cff5b1f
feat: rescueTokens implementation + tests
yan-man Sep 5, 2024
4d2abe2
fix: convert to Ownable
yan-man Sep 6, 2024
2faf8a6
fix: validate calculated ghoAmount matches ghoSold, reset approvals, …
yan-man Sep 6, 2024
bc05a7d
test: create mockGSM to test unmatching gho amount case
yan-man Sep 6, 2024
0c1beb2
refactor: methods re-order
yan-man Sep 6, 2024
117eda8
refactor: rename mock
yan-man Sep 6, 2024
4f4603c
test: test coverage for remaining token balances
yan-man Sep 6, 2024
1dbff49
feat: init buyAssetWithSig
yan-man Sep 6, 2024
bce34e7
test: happy path test for buyAssetWithSig
yan-man Sep 7, 2024
62657ee
test: simplify testBuyAssetWithSig, add testBuyAssetWithSigExactDeadline
yan-man Sep 9, 2024
522ada9
test: add testRevertBuyAssetWithSigExpiredSignature
yan-man Sep 9, 2024
b60751d
test: add testRevertBuyAssetWithSigInvalidSignature
yan-man Sep 9, 2024
6d90a66
test: add testRevertBuyAssetWithSigInvalidAmount
yan-man Sep 9, 2024
ea90912
test: refer to proper value for BUIDL gsm exposure
yan-man Sep 9, 2024
c9140c5
test: resolve test assertion on remaining gsm gho balance
yan-man Sep 9, 2024
14fc942
test: clean up assertion failure messages, reference different test v…
yan-man Sep 9, 2024
0ba7d24
refactor: code segmentation
yan-man Sep 9, 2024
1d0e326
test: fuzz test on minAssetAmount values
yan-man Sep 9, 2024
fdd0046
feat: init sellAsset functionality with mock
yan-man Sep 10, 2024
4e40559
refactor: rename vars, update docs
yan-man Sep 10, 2024
aa30e1b
test: add docs for mock redemption
yan-man Sep 10, 2024
3b96732
feat: skeleton for sellAsset implementation
yan-man Sep 10, 2024
d79ebde
feat: add remappings for mocks
yan-man Sep 10, 2024
80a2c72
test: resolve test compilations with additional constructor param
yan-man Sep 10, 2024
2f94eaa
test: happy path sellAsset emitting events
yan-man Sep 10, 2024
206160e
test: add happy path sellAsset assertions
yan-man Sep 10, 2024
acc78ca
test: invalid redemption
yan-man Sep 10, 2024
59d7291
test: fail cases for sellAsset with invalid asset amounts, add mocks
yan-man Sep 10, 2024
71b8641
refactor: rename redeemable asset to issued asset to be more correct
yan-man Sep 10, 2024
43187e6
test: declare new typehash specific for converter, init happy path se…
yan-man Sep 10, 2024
cd76351
refactor: reference mock contract directly
yan-man Sep 10, 2024
cb7b33b
refactor: re-order assertions
yan-man Sep 10, 2024
15e862a
test: fail cases for sellAssetWithSig
yan-man Sep 11, 2024
409944b
refactor: capitalize variable name
yan-man Sep 11, 2024
7b96c4c
refactor: use more precise variable reference
yan-man Sep 11, 2024
74da86e
test: add testRevertSellAssetWithSigInvalidAmount
yan-man Sep 11, 2024
f0c35fd
test: add testBuyAssetWithDonatedTokens
yan-man Sep 11, 2024
57df1d7
test: add testSellAssetDonatedTokens
yan-man Sep 11, 2024
4b1ed4e
test: fuzz for donated token amounts
yan-man Sep 11, 2024
fdc083e
test: clean up var references and add testFuzzSellAssetMaxAmount
yan-man Sep 11, 2024
df84a21
test: clean up var reference
yan-man Sep 11, 2024
91fb7b9
test: add fuzz tests, testRevertSellAssetWithSigInvalidNonce
yan-man Sep 11, 2024
655b0b0
test: add testRevertBuyAssetWithSigInvalidNonce
yan-man Sep 11, 2024
d0f89f2
test: remove mock test files from coverage
yan-man Sep 11, 2024
8a47c39
fix: remove need to upgrade gsm to trigger error
yan-man Sep 18, 2024
6e208a2
Merge branch 'main' into feat/gsm-buidl
yan-man Sep 18, 2024
3d7cbb0
feat: init GsmConverter overview doc
yan-man Sep 24, 2024
feeda78
test: reduce unneeded mock code for MockGsmFailedSellAssetRemainingGh…
yan-man Sep 25, 2024
53dc395
refactor: rename subscription event
yan-man Oct 14, 2024
b48a0be
test: resolve tests from event change, rename mock files to be BUIDL-…
yan-man Oct 14, 2024
ae2dec4
Merge branch 'main' into feat/gsm-buidl
yan-man Oct 14, 2024
72ee91d
refactor: rename issuance to subscription
yan-man Oct 14, 2024
9a686d3
test: add mock USTB subscription/redemption contract
yan-man Oct 14, 2024
16a5d59
feat: add todo notes on subscription fees
yan-man Oct 14, 2024
c943e73
fix: account for subscription fee, resolve tests, param names
yan-man Oct 14, 2024
0201eea
test: end prank
yan-man Oct 15, 2024
a32f7ee
Merge branch 'feat/gsm-buidl' into feat/gsm-ustb
yan-man Oct 15, 2024
a7a828f
test: update mock comments
yan-man Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/buidl-gsm-converter.md
Original file line number Diff line number Diff line change
@@ -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, ...
Binary file added docs/img/buyAsset.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
277 changes: 277 additions & 0 deletions src/contracts/facilitators/gsm/converter/GsmConverter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// 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 {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';
import {IGsmConverter} from './interfaces/IGsmConverter.sol';
import {IRedemption} from '../dependencies/circle/IRedemption.sol';
// TODO: replace with proper issuance implementation later
import {MockBUIDLSubscription} from '../../../../test/mocks/MockBUIDLSubscription.sol';

import 'forge-std/console2.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 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
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;

/// @inheritdoc IGsmConverter
address public immutable GSM;

/// @inheritdoc IGsmConverter
address public immutable ISSUED_ASSET;

/// @inheritdoc IGsmConverter
address public immutable REDEEMED_ASSET;

/// @inheritdoc IGsmConverter
address public immutable REDEMPTION_CONTRACT;

/// @inheritdoc IGsmConverter
address public immutable SUBSCRIPTION_CONTRACT;

/// @inheritdoc IGsmConverter
mapping(address => uint256) public nonces;

/**
* @dev Constructor
* @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 issuedAsset The address of the asset being redeemed
* @param redeemedAsset The address of the asset being received from redemption
*/
constructor(
address admin,
address gsm,
address redemptionContract,
address issuanceReceiverContract,
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(issuedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID');
require(redeemedAsset != address(0), 'ZERO_ADDRESS_NOT_VALID');

GSM = gsm;
REDEMPTION_CONTRACT = redemptionContract;
SUBSCRIPTION_CONTRACT = issuanceReceiverContract;
ISSUED_ASSET = issuedAsset; // BUIDL
REDEEMED_ASSET = redeemedAsset; // USDC
GHO_TOKEN = IGsm(GSM).GHO_TOKEN();

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');

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);
}

/// @inheritdoc IGsmConverter
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(originator, maxAmount, receiver);
}

/// @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);
}

/// @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,
address receiver
) internal returns (uint256, uint256) {
uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).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 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), boughtAssetAmount);
IRedemption(REDEMPTION_CONTRACT).redeem(boughtAssetAmount);
// Redemption exchanges in 1:1 ratio between BUIDL and USDC
require(
IERC20(REDEEMED_ASSET).balanceOf(address(this)) ==
initialRedeemedAssetBalance + boughtAssetAmount,
'INVALID_REDEMPTION'
);
IERC20(ISSUED_ASSET).approve(address(REDEMPTION_CONTRACT), 0);
IERC20(REDEEMED_ASSET).safeTransfer(receiver, boughtAssetAmount);

require(
IGhoToken(GHO_TOKEN).balanceOf(address(this)) == initialGhoBalance,
'INVALID_REMAINING_GHO_BALANCE'
);
require(
IERC20(ISSUED_ASSET).balanceOf(address(this)) == initialIssuedAssetBalance,
'INVALID_REMAINING_ISSUED_ASSET_BALANCE'
);

emit BuyAssetThroughRedemption(originator, receiver, boughtAssetAmount, ghoSold);
return (boughtAssetAmount, 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) {
uint256 initialGhoBalance = IGhoToken(GHO_TOKEN).balanceOf(address(this));
uint256 initialIssuedAssetBalance = IERC20(ISSUED_ASSET).balanceOf(address(this));
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).approve(SUBSCRIPTION_CONTRACT, assetAmount);
//TODO: replace with proper issuance implementation later
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 + subscribedAssetAmount,
'INVALID_ISSUANCE'
);
// reset approval after issuance
IERC20(REDEEMED_ASSET).approve(SUBSCRIPTION_CONTRACT, 0);

// TODO: account for fees for sellAsset amount param
(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'
);
require(
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, soldAssetAmount, ghoBought);
return (soldAssetAmount, ghoBought);
}
}
Loading
Loading