Skip to content

Commit

Permalink
feat: expose latest answer on static a token (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
sakulstra committed Aug 8, 2024
1 parent 4ad98c0 commit ca640a8
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 2 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions src/periphery/contracts/static-a-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
17 changes: 15 additions & 2 deletions src/periphery/contracts/static-a-token/StaticATokenLM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -67,6 +70,7 @@ contract StaticATokenLM is
_disableInitializers();
POOL = pool;
INCENTIVES_CONTROLLER = rewardsController;
POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER();
}

modifier onlyPauseGuardian() {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
29 changes: 29 additions & 0 deletions tests/periphery/static-a-token/StaticATokenLM.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit ca640a8

Please sign in to comment.