Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
51 changes: 51 additions & 0 deletions src/contracts/extensions/price-adaptors/ScaledPriceAdaptor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

import {AggregatorInterface} from '../../dependencies/chainlink/AggregatorInterface.sol';
import {IScaledPriceAdaptor} from '../../interfaces/IScaledPriceAdaptor.sol';

/**
* @title ScaledPriceAdaptor
* @author Aave Labs
* @dev Price Adaptor for Chainlink price feeds with non standard decimal USD feeds to 8 decimals.
*/
contract ScaledPriceAdaptor is IScaledPriceAdaptor {
AggregatorInterface internal immutable _SOURCE;

uint8 internal constant _BASE_DECIMALS = 8;
bool internal immutable _SCALE_UP;
uint256 internal immutable _SCALE;

constructor(address source_) {
_SOURCE = AggregatorInterface(source_);
uint8 sourceDecimals = _SOURCE.decimals();
_SCALE_UP = sourceDecimals < _BASE_DECIMALS;
_SCALE = 10 ** (_SCALE_UP ? _BASE_DECIMALS - sourceDecimals : sourceDecimals - _BASE_DECIMALS);
}

/// @inheritdoc IScaledPriceAdaptor
function latestAnswer() external view returns (int256) {
return
_SCALE_UP ? _SOURCE.latestAnswer() * int256(_SCALE) : _SOURCE.latestAnswer() / int256(_SCALE);
}

/// @inheritdoc IScaledPriceAdaptor
function description() external view returns (string memory) {
return string.concat(_SOURCE.description(), ' (USD Scaled)');
Copy link

@avniculae avniculae Jul 16, 2025

Choose a reason for hiding this comment

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

why USD? should we omit it?

Copy link
Member Author

Choose a reason for hiding this comment

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

'USD' unit for the 8 decimals (that we've fixed here)
most RWA feeds are 'NAV' base unit which isn't consistent with decimals

}

/// @inheritdoc IScaledPriceAdaptor
function decimals() external pure returns (uint8) {
return _BASE_DECIMALS;
}

/// @inheritdoc IScaledPriceAdaptor
function scale() external view returns (bool, uint256) {
return (_SCALE_UP, _SCALE);
}

/// @inheritdoc IScaledPriceAdaptor
function source() external view returns (address) {
return address(_SOURCE);
}
}
33 changes: 33 additions & 0 deletions src/contracts/interfaces/IScaledPriceAdaptor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

interface IScaledPriceAdaptor {
/**
* @dev Units and direction used to scale answer to base decimals of 8.
* @dev Looses price precision when scaling down by log10(scaleUnits).
* @return scaleUp Whether to scale up or down.
* @return scaleUnits The units to scale by.
*/
function scale() external view returns (bool scaleUp, uint256 scaleUnits);

/**
* @dev The decimals of price adaptor.
*/
function decimals() external view returns (uint8);

/**
* @dev Underlying chainlink price source.
*/
function source() external view returns (address);

/**
* @dev Description of price adaptor.
*/
function description() external view returns (string memory);

/**
* @dev Scaled `latestAnswer` from chainlink price feed.
* @dev Looses price precision when scaling down by log10(scaleUnits).
*/
function latestAnswer() external view returns (int256);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ contract MockAggregator {
return 1;
}

function decimals() external pure returns (uint8) {
function decimals() external view virtual returns (uint8) {
return 8;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import {MockAggregator} from './MockAggregator.sol';

contract MockAggregatorMetadata is MockAggregator {
uint8 internal immutable _DECIMALS;

constructor(int256 initialAnswer_, uint8 decimals_) MockAggregator(initialAnswer_) {
_DECIMALS = decimals_;
}

function decimals() external view override returns (uint8) {
return _DECIMALS;
}
}
38 changes: 38 additions & 0 deletions tests/extensions/price-adaptors/ScaledPriceAdaptor.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {MockAggregatorMetadata} from '../../../src/contracts/mocks/oracle/CLAggregators/MockAggregatorMetadata.sol';
import {ScaledPriceAdaptor} from '../../../src/contracts/extensions/price-adaptors/ScaledPriceAdaptor.sol';
import {TestnetProcedures} from '../../utils/TestnetProcedures.sol';

contract ScaledPriceAdaptorTests is TestnetProcedures {
function test_adaptor_less_than_base() public {
test_fuzz_adaptor({sourceDecimals: 2, price: 1e2});
test_fuzz_adaptor({sourceDecimals: 6, price: 32.323e6});
}

function test_adaptor_greater_than_base() public {
test_fuzz_adaptor({sourceDecimals: 12, price: 1e12});
}

function test_adaptor_equal_to_base() public {
test_fuzz_adaptor({sourceDecimals: 8, price: 1e8});
}

function test_fuzz_adaptor(uint256 sourceDecimals, int256 price) public {
sourceDecimals = bound(sourceDecimals, 1, 36);
price = bound(price, 1, int256(10 ** sourceDecimals));
address source = address(new MockAggregatorMetadata(price, uint8(sourceDecimals)));
ScaledPriceAdaptor adaptor = new ScaledPriceAdaptor(source);

(bool scaleUp, uint256 scale) = adaptor.scale();
assertEq(adaptor.decimals(), 8);
assertEq(scaleUp, adaptor.decimals() > sourceDecimals);
assertEq(
scale,
10 ** (scaleUp ? adaptor.decimals() - sourceDecimals : sourceDecimals - adaptor.decimals())
);
assertEq(adaptor.latestAnswer(), scaleUp ? price * int256(scale) : price / int256(scale));
assertEq(adaptor.source(), source);
}
}
8 changes: 6 additions & 2 deletions tests/mocks/AaveV3TestListing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
pragma solidity ^0.8.0;

import '../../src/contracts/extensions/v3-config-engine/AaveV3Payload.sol';
import {ScaledPriceAdaptor} from '../../src/contracts/extensions/price-adaptors/ScaledPriceAdaptor.sol';
import {TestnetRwaERC20} from '../../src/contracts/mocks/testnet-helpers/TestnetRwaERC20.sol';
import {TestnetERC20} from '../../src/contracts/mocks/testnet-helpers/TestnetERC20.sol';
import {MockAggregator} from '../../src/contracts/mocks/oracle/CLAggregators/MockAggregator.sol';
import {MockAggregatorMetadata} from '../../src/contracts/mocks/oracle/CLAggregators/MockAggregatorMetadata.sol';
import {ACLManager} from '../../src/contracts/protocol/configuration/ACLManager.sol';
import {MarketReport} from '../../src/deployments/interfaces/IMarketReportTypes.sol';
import {IPoolConfigurator, ConfiguratorInputTypes} from '../../src/contracts/interfaces/IPoolConfigurator.sol';
Expand Down Expand Up @@ -62,10 +64,12 @@ contract AaveV3TestListing is AaveV3Payload {
WETH_MOCK_PRICE_FEED = address(new MockAggregator(1800e8));

BUIDL_ADDRESS = address(new TestnetRwaERC20('BUIDL', 'BUIDL', 6, erc20Owner));
BUIDL_MOCK_PRICE_FEED = address(new MockAggregator(1e8));
BUIDL_MOCK_PRICE_FEED = address(
new ScaledPriceAdaptor(address(new MockAggregatorMetadata(1e2, 2)))
);

USTB_ADDRESS = address(new TestnetRwaERC20('USTB', 'USTB', 6, erc20Owner));
USTB_MOCK_PRICE_FEED = address(new MockAggregator(10e8));
USTB_MOCK_PRICE_FEED = address(new ScaledPriceAdaptor(address(new MockAggregator(10e8))));

WTGXX_ADDRESS = address(new TestnetRwaERC20('WTGXX', 'WTGXX', 18, erc20Owner));
WTGXX_MOCK_PRICE_FEED = address(new MockAggregator(1e8));
Expand Down
Loading