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

Separate Stata4626 #8

Merged
merged 43 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c225aa5
Separate Stata4626
kyzia551 Aug 13, 2024
4475a18
change to erc7201
kyzia551 Aug 13, 2024
863485f
regenerated storage location
kyzia551 Aug 13, 2024
6034a94
change latestAnswer calculation logic
kyzia551 Aug 13, 2024
0ecfd37
DRAFT: Refactoring in extensions style
kyzia551 Aug 14, 2024
b651f7f
add initializer
kyzia551 Aug 14, 2024
8e2a8a8
remove unused params at __Stata4626_init
kyzia551 Aug 14, 2024
87b5390
remove RayMathExplicitRounding
kyzia551 Aug 14, 2024
7c0e5b6
regenerated ERC20AaveLMStorageLocation
kyzia551 Aug 14, 2024
721b437
add RAY constant
kyzia551 Aug 14, 2024
9b6691f
remove IInitializableStata4626LM
kyzia551 Aug 14, 2024
482f8ca
depositWithPermit
kyzia551 Aug 14, 2024
4434e4f
disclamer on _update overload
kyzia551 Aug 14, 2024
b596ccc
some descriptions cleanup
kyzia551 Aug 14, 2024
09d6ec2
change require to revert
kyzia551 Aug 14, 2024
1ad079d
add comment to latestAnswer calc
kyzia551 Aug 14, 2024
3021817
add comment to latestAnswer calc -1
kyzia551 Aug 14, 2024
2fffe82
make ERC20AaveLMUpgradable abstract
kyzia551 Aug 14, 2024
08c95d1
update license
kyzia551 Aug 14, 2024
cef08a2
rename merger and 4626 contracts
kyzia551 Aug 14, 2024
915282d
change Upgradable to Upgradeable
kyzia551 Aug 14, 2024
7fbb149
move _disableInitializers into StataTokenV2
kyzia551 Aug 14, 2024
89d258d
rename IStata4626 to IERC4626StataToken
kyzia551 Aug 14, 2024
24ede0b
rename init on ERC4626StataToken
kyzia551 Aug 14, 2024
66b8fec
Changes on stata initializations, to follow more strict guidelines
eboadom Aug 14, 2024
4035b6f
Changes to make stata more consistent with using ERC20 extensions
eboadom Aug 14, 2024
425c7db
Fix on function called on initialize of stata
eboadom Aug 14, 2024
589abf9
Merge pull request #11 from bgd-labs/feat/stata-oz-extensions-standards
kyzia551 Aug 14, 2024
114be98
feat: improved tests
sakulstra Aug 15, 2024
87cee0e
fix: update test
sakulstra Aug 15, 2024
fbaa45f
feat: add erc4626 tests
sakulstra Aug 15, 2024
3fe5ae2
fix: migrate some more tests
sakulstra Aug 15, 2024
1906609
fix: improve tests
sakulstra Aug 15, 2024
588e1fc
refactor: move to dedicated files
sakulstra Aug 15, 2024
bd5b9a9
feat: improve tests
sakulstra Aug 15, 2024
0698a73
fix typo
kyzia551 Aug 16, 2024
37b55db
feat: add permit tests
sakulstra Aug 16, 2024
174cd79
fix: linting
sakulstra Aug 16, 2024
617be16
feat: improved docs
sakulstra Aug 16, 2024
9cf1cc0
fix: typos
sakulstra Aug 16, 2024
c6befb0
Merge pull request #12 from bgd-labs/feat/improved-tests
kyzia551 Aug 16, 2024
c193fa8
fix: use internal function
sakulstra Aug 16, 2024
dca9d8c
Merge pull request #9 from bgd-labs/ref/to-oz-extensions
sakulstra Aug 16, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.0;

import '../../interfaces/IMarketReportTypes.sol';
import {TransparentProxyFactory, ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol';
import {StaticATokenLM} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenLM.sol';
import {Stata4626LM} from 'aave-v3-periphery/contracts/static-a-token/Stata4626LM.sol';
import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol';
import {IErrors} from '../../interfaces/IErrors.sol';

Expand All @@ -17,7 +17,7 @@ contract AaveV3HelpersProcedureTwo is IErrors {

staticATokenReport.transparentProxyFactory = address(new TransparentProxyFactory());
staticATokenReport.staticATokenImplementation = address(
new StaticATokenLM(IPool(pool), IRewardsController(rewardsController))
new Stata4626LM(IPool(pool), IRewardsController(rewardsController))
);
staticATokenReport.staticATokenFactoryImplementation = address(
new StaticATokenFactory(
Expand Down
18 changes: 9 additions & 9 deletions src/periphery/contracts/static-a-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The static-a-token contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-462
- **Upgradable by the Aave governance.** Similar to other contracts of the Aave ecosystem, the Level 1 executor (short executor) will be able to add new features to the deployed instances of the `stataTokens`.
- **Powered by a stataToken Factory.** Whenever a token will be listed on Aave v3, anybody will be able to call the stataToken Factory to deploy an instance for the new asset, permissionless, but still assuring the code used and permissions are properly configured without any extra headache.

See [IStaticATokenLM.sol](./interfaces/IStaticATokenLM.sol) for detailed method documentation.
See [IStata4626LM.sol](./interfaces/IStata4626LM.sol) for detailed method documentation.

## Deployed Addresses

Expand Down Expand Up @@ -76,9 +76,9 @@ It is important to note that:

```
git checkout main
forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageBefore.md
forge inspect src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM storage-layout --pretty > reports/StaticATokenStorageBefore.md
git checkout project-a
forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageAfter.md
forge inspect src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM storage-layout --pretty > reports/StaticATokenStorageAfter.md
make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff
```

Expand All @@ -90,12 +90,12 @@ index a7e3105..89e0967 100644
@@ -1,7 +1,6 @@
| Name | Type | Slot | Offset | Bytes | Contract |
| ------------------ | ------------------------------------------------------------------------------ | ---- | ------ | ----- | ------------------------------------------------------------------------ |
-| \_initialized | uint8 | 0 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
-| \_initializing | bool | 0 | 1 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
+| \_\_deprecated | uint256 | 0 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
| name | string | 1 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
| symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
| decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM |
-| \_initialized | uint8 | 0 | 0 | 1 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
-| \_initializing | bool | 0 | 1 | 1 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
+| \_\_deprecated | uint256 | 0 | 0 | 32 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
| name | string | 1 | 0 | 32 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
| symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
| decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/Stata4626LM.sol:Stata4626LM |
```

### Umbrella upgrade plan
Expand Down
303 changes: 303 additions & 0 deletions src/periphery/contracts/static-a-token/Stata4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol';
import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol';
import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol';
import {ERC20PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PausableUpgradeable.sol';
import {ERC4626Upgradeable, Math, IERC4626} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol';
import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol';
import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol';

import {IPool} from '../../../core/contracts/interfaces/IPool.sol';
import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol';
import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol';
import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol';
import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol';
import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol';

import {IAToken} from './interfaces/IAToken.sol';
import {RayMathExplicitRounding} from '../libraries/RayMathExplicitRounding.sol';
import {IStata4626} from './interfaces/IStata4626.sol';

/**
* @title Stata4626
* @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive
* a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate.
* @author BGD labs
*/
contract Stata4626 is
ERC20Upgradeable,
ERC20PermitUpgradeable,
ERC20PausableUpgradeable,
ERC4626Upgradeable,
Rescuable,
IStata4626
{
using RayMathExplicitRounding for uint256;

/// @custom:storage-location erc7201:aave-dao.storage.Stata4626
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the erc7201 prefix? I don't see that included in the hash calculation for the slot

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://eips.ethereum.org/EIPS/eip-7201 because that's how it's supposed to be done. The erc2701 prefix helps with generating some artifacts it think.

struct Stata4626Storage {
IERC20 _aToken;
}

// keccak256(abi.encode(uint256(keccak256("aave-dao.storage.Stata4626")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant Stata4626StorageLocation =
0x4865e395e8f896b2ca01e8489fd809975f6c70c69fd3d1cf5d2263a21a649200;

function _getStata4626Storage() private pure returns (Stata4626Storage storage $) {
assembly {
$.slot := Stata4626StorageLocation
}
}

IPool public immutable POOL;
IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER;

constructor(IPool pool) {
_disableInitializers();
POOL = pool;
POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER();
}

// TODO: maybe I should not put calls to initializers here
function __Stata4626_init(
address newAToken,
string calldata staticATokenName,
string calldata staticATokenSymbol
) internal onlyInitializing {
require(IAToken(newAToken).POOL() == address(POOL));

IERC20 aTokenUnderlying = IERC20(IAToken(newAToken).UNDERLYING_ASSET_ADDRESS());

__ERC20_init(staticATokenName, staticATokenSymbol);
__ERC20Permit_init(staticATokenName);
__ERC4626_init(aTokenUnderlying);
__ERC20Pausable_init();
sakulstra marked this conversation as resolved.
Show resolved Hide resolved

Stata4626Storage storage $ = _getStata4626Storage();
$._aToken = IERC20(newAToken);

SafeERC20.forceApprove(aTokenUnderlying, address(POOL), type(uint256).max);
}

modifier onlyPauseGuardian() {
if (!canPause(_msgSender())) revert OnlyPauseGuardian(_msgSender());
_;
}

function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) {
sakulstra marked this conversation as resolved.
Show resolved Hide resolved
return ERC4626Upgradeable.decimals();
}
///@inheritdoc IStata4626
function canPause(address actor) public view returns (bool) {
return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor);
}

/// @inheritdoc IRescuable
function whoCanRescue() public view override returns (address) {
return POOL_ADDRESSES_PROVIDER.getACLAdmin();
}

///@inheritdoc IStata4626
function depositATokens(uint256 assets, address receiver) public returns (uint256) {
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares, false);

return shares;
}

///@inheritdoc IStata4626
function redeemATokens(uint256 shares, address receiver, address owner) public returns (uint256) {
uint256 assets = previewRedeem(shares);
_withdraw(_msgSender(), receiver, owner, shares, assets, false);

return assets;
}

///@inheritdoc IStata4626
function setPaused(bool paused) external onlyPauseGuardian {
if (paused) _pause();
else _unpause();
}

///@inheritdoc IStata4626
function aToken() public view returns (IERC20) {
Stata4626Storage storage $ = _getStata4626Storage();
return $._aToken;
}

///@inheritdoc IERC4626
function maxMint(address) public view override returns (uint256) {
uint256 assets = maxDeposit(address(0));
if (assets == type(uint256).max) return type(uint256).max;
return convertToShares(assets);
}

///@inheritdoc IERC4626
function maxWithdraw(address owner) public view override returns (uint256) {
return convertToAssets(maxRedeem(owner));
}

///@inheritdoc IERC4626
function maxRedeem(address owner) public view override returns (uint256) {
DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(asset());

// if paused or inactive users cannot withdraw underlying
if (
!ReserveConfiguration.getActive(reserveData.configuration) ||
ReserveConfiguration.getPaused(reserveData.configuration)
) {
return 0;
}

// otherwise users can withdraw up to the available amount
uint256 underlyingTokenBalanceInShares = convertToShares(reserveData.virtualUnderlyingBalance);
uint256 cachedUserBalance = balanceOf(owner);
return
underlyingTokenBalanceInShares >= cachedUserBalance
? cachedUserBalance
: underlyingTokenBalanceInShares;
}

///@inheritdoc IERC4626
function maxDeposit(address) public view override returns (uint256) {
DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(asset());

// if inactive, paused or frozen users cannot deposit underlying
if (
!ReserveConfiguration.getActive(reserveData.configuration) ||
ReserveConfiguration.getPaused(reserveData.configuration) ||
ReserveConfiguration.getFrozen(reserveData.configuration)
) {
return 0;
}

uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) *
(10 ** ReserveConfiguration.getDecimals(reserveData.configuration));
// if no supply cap deposit is unlimited
if (supplyCap == 0) return type(uint256).max;
// return remaining supply cap margin
uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() +
reserveData.accruedToTreasury).rayMulRoundUp(_rate());
return currentSupply > supplyCap ? 0 : supplyCap - currentSupply;
}

///@inheritdoc IStata4626
function latestAnswer() external view returns (int256) {
uint256 aTokenUnderlyingAssetPrice = IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle())
.getAssetPrice(asset());
return int256(convertToAssets(aTokenUnderlyingAssetPrice));
}

function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares,
bool depositToAave
) internal virtual {
if (shares == 0) {
revert StaticATokenInvalidZeroShares();
}
// If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth

if (depositToAave) {
address cachedAsset = asset();
SafeERC20.safeTransferFrom(IERC20(cachedAsset), caller, address(this), assets);
POOL.deposit(cachedAsset, assets, address(this), 0);
} else {
Stata4626Storage storage $ = _getStata4626Storage();
SafeERC20.safeTransferFrom($._aToken, caller, address(this), assets);
}
_mint(receiver, shares);

emit Deposit(caller, receiver, assets, shares);
}

function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares
) internal virtual override {
_deposit(caller, receiver, assets, shares, true);
}

function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares,
bool withdrawFromAave
) internal virtual {
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}

// If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
_burn(owner, shares);
if (withdrawFromAave) {
POOL.withdraw(asset(), assets, receiver);
} else {
Stata4626Storage storage $ = _getStata4626Storage();
SafeERC20.safeTransfer($._aToken, receiver, assets);
}

emit Withdraw(caller, receiver, owner, assets, shares);
}

function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual override {
_withdraw(caller, receiver, owner, assets, shares, true);
}

function _convertToShares(
uint256 assets,
Math.Rounding rounding
) internal view virtual override returns (uint256) {
// @dev we use unsignedRoundsUp instead of just simple comparison to be sure that the code will work as expected
// in case Math.Rounding.Trunc or Math.Rounding.Expand will be passed
if (Math.unsignedRoundsUp(rounding)) return assets.rayDivRoundUp(_rate());
return assets.rayDivRoundDown(_rate());
}

function _convertToAssets(
uint256 shares,
Math.Rounding rounding
) internal view virtual override returns (uint256) {
// @dev we use unsignedRoundsUp instead of just simple comparison to be sure that the code will work as expected
// in case Math.Rounding.Trunc or Math.Rounding.Expand will be passed
if (Math.unsignedRoundsUp(rounding)) return shares.rayMulRoundUp(_rate());
return shares.rayMulRoundDown(_rate());
}

function _update(
address from,
address to,
uint256 amount
) internal virtual override(ERC20Upgradeable, ERC20PausableUpgradeable) whenNotPaused {
super._update(from, to, amount);
}

function _rate() internal view returns (uint256) {
return POOL.getReserveNormalizedIncome(asset());
}
}
Loading