diff --git a/Makefile b/Makefile index f46c33c3..2708df60 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,5 @@ coverage :; forge coverage --report lcov && \ download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} git-diff : @mkdir -p diffs + @npx prettier ${before} ${after} --write @printf '%s\n%s\n%s\n' "\`\`\`diff" "$$(git diff --no-index --diff-algorithm=patience --ignore-space-at-eol ${before} ${after})" "\`\`\`" > diffs/${out}.md diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 9ced57a6..f5dddf20 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -36,3 +36,20 @@ For this project, the security procedures applied/being finished are: - The test suite of the codebase itself. - Certora audit/property checking for all the dynamics of the `stataToken`, including respecting all the specs of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626). + +## Upgrade Notes Umbrella + +- Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. +- The static A tokens are given a `rescuable`, which can be used by the ACL admin to rescue tokens locked to the contract. +- Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. Even if someone were to frontrun the permit via mempool observation the permit is wrapped in a `try..catch` to prevent griefing attacks. +- The static a token not implements pausability, which allows the ACL admin to pause all transfers. + +The storage layout diff was generated via: + +``` +git checkout main +forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM 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 +make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff +``` diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 5ca0fb29..80f40ef4 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.10; 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 {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; @@ -55,6 +57,7 @@ contract StaticATokenLM is uint256 public constant STATIC__ATOKEN_LM_REVISION = 3; IPool public immutable POOL; + IPoolAddressesProvider immutable POOL_ADDRESSES_PROVIDER; IRewardsController public immutable INCENTIVES_CONTROLLER; IERC20 internal _aToken; @@ -67,6 +70,7 @@ contract StaticATokenLM is _disableInitializers(); POOL = pool; INCENTIVES_CONTROLLER = rewardsController; + POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); } modifier onlyPauseGuardian() { @@ -75,7 +79,7 @@ contract StaticATokenLM is } function canPause(address actor) public view returns (bool) { - return IACLManager(POOL.ADDRESSES_PROVIDER().getACLManager()).isEmergencyAdmin(actor); + return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); } ///@inheritdoc IInitializableStaticATokenLM @@ -103,7 +107,7 @@ contract StaticATokenLM is /// @inheritdoc IRescuable function whoCanRescue() public view override returns (address) { - return POOL.ADDRESSES_PROVIDER().getACLAdmin(); + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); } ///@inheritdoc IStaticATokenLM @@ -468,6 +472,15 @@ contract StaticATokenLM is return _withdraw(owner, receiver, shares, 0, withdrawFromAave); } + ///@inheritdoc IStaticATokenLM + function latestAnswer() external view returns (int256) { + return + int256( + (IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()).getAssetPrice(_aTokenUnderlying) * + POOL.getReserveNormalizedIncome(_aTokenUnderlying)) / 1e27 + ); + } + function _deposit( address depositor, address receiver, diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index 18edaeb7..2fbdd9cf 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -219,4 +219,15 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { * @param paused boolean determining if the token should be paused or unpaused */ function setPaused(bool paused) external; + + /** + * @notice Returns the current asset price of the stataToken. + * The price is calculated as `underlying_price * exchangeRate`. + * It is important to note that: + * - `underlying_price` is the price obtained by the aave-oracle and is subject to it's internal pricing mechanisms. + * - as the price is scaled over the exchangeRate, but maintains the same precision as the underlying the price might be underestimated by 1 unit. + * - when pricing multiple `shares` as `shares * price` keep in mind that the error compounds. + * @return price the current asset price. + */ + function latestAnswer() external view returns (int256); } diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol index 6f2ddaf4..a8a23c32 100644 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -10,6 +10,7 @@ import {RayMathExplicitRounding} from '../../../src/periphery/contracts/librarie import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; import {SigUtils} from '../../utils/SigUtils.sol'; import {BaseTest, TestnetERC20} from './TestBase.sol'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; contract StaticATokenLMTest is BaseTest { using RayMathExplicitRounding for uint256; @@ -48,6 +49,34 @@ contract StaticATokenLMTest is BaseTest { ); } + function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { + vm.mockCall( + address(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(1e27) + ); + uint256 stataPrice = uint256(staticATokenLM.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); + assertEq(stataPrice, underlyingPrice); + } + + function test_latestAnswer_priceShouldReflectIndexAccrual(uint256 liquidityIndex) public { + liquidityIndex = bound(liquidityIndex, 1e27, 1e29); + vm.mockCall( + address(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(liquidityIndex) + ); + uint256 stataPrice = uint256(staticATokenLM.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); + uint256 expectedStataPrice = (underlyingPrice * liquidityIndex) / 1e27; + assertEq(stataPrice, expectedStataPrice); + + // reverse the math to ensure precision loss is within bounds + uint256 reversedUnderlying = (stataPrice * 1e27) / liquidityIndex; + assertApproxEqAbs(underlyingPrice, reversedUnderlying, 1); + } + function test_convertersAndPreviews() public view { uint128 amount = 5 ether; uint256 shares = staticATokenLM.convertToShares(amount);