Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 22 additions & 1 deletion contracts/contracts/extensions/Interfaces/IReserve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,43 @@ interface IReserve {
*/
event ReserveAddressChanged(address indexed previousAddress, address indexed newAddress);

/**
* @dev Emitted when the updatedAt Threshold for the reserve is changed
*
* @param previousThreshold The previous updatedAt Threshold
* @param newThreshold The new updatedAt Threshold
*/
event UpdatedAtThresholdChanged(uint256 indexed previousThreshold, uint256 indexed newThreshold);

/**
* @dev Changes the current reserve address
*
* @param newAddress The new reserve address
*/
function updateReserveAddress(address newAddress) external;

/**
* @dev Changes the current updatedAt threshold limit
*
* @param newThreshold The new updatedAt threshold
*/
function updateUpdatedAtThreshold(uint256 newThreshold) external;

/**
* @dev Get the current reserve amount
*
*/
function getReserveAmount() external view returns (int256);
function getReserveAmount() external view returns (int256 amount, uint256 updatedAt);

/**
* @dev Gets the current reserve address
*
*/
function getReserveAddress() external view returns (address);

/**
* @dev Gets the current updatedAt threshold
*
*/
function getUpdatedAtThreshold() external view returns (uint256);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ interface IReserveStorageWrapper {
* @param amount The value to check
*/
error FormatNumberIncorrect(uint256 amount);

/**
* @dev Emitted when the provided reserve `amount` is too old
*
* @param updatedAt The updated At value for the reserve amount
* @param updatedAtThreshold The updated At Threshold value for the reserve amount
*/
error ReserveAmountOutdated(uint256 updatedAt, uint256 updatedAtThreshold);
}
30 changes: 21 additions & 9 deletions contracts/contracts/extensions/ReserveFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ import {IStaticFunctionSelectors} from '../resolver/interfaces/resolverProxy/ISt
import {ADMIN_ROLE} from '../constants/roles.sol';

contract ReserveFacet is IReserve, IStaticFunctionSelectors, ReserveStorageWrapper {
/**
* @dev Gets the current reserve amount
*
*/
function getReserveAmount() external view override(IReserve) returns (int256) {
return _getReserveAmount();
}

/**
* @dev Updates de reserve address
*
Expand All @@ -27,6 +19,20 @@ contract ReserveFacet is IReserve, IStaticFunctionSelectors, ReserveStorageWrapp
emit ReserveAddressChanged(previous, newAddress);
}

function updateUpdatedAtThreshold(uint256 newThreshold) external override(IReserve) onlyRole(ADMIN_ROLE) {
uint256 previous = _getUpdatedAtThreshold();
_reserveStorage().updatedAtThreshold = newThreshold;
emit UpdatedAtThresholdChanged(previous, newThreshold);
}

/**
* @dev Gets the current reserve amount
*
*/
function getReserveAmount() external view override(IReserve) returns (int256 amount, uint256 updatedAt) {
return _getReserveAmount();
}

/**
* @dev Gets the current reserve address
*
Expand All @@ -35,16 +41,22 @@ contract ReserveFacet is IReserve, IStaticFunctionSelectors, ReserveStorageWrapp
return _reserveStorage().reserveAddress;
}

function getUpdatedAtThreshold() external view returns (uint256) {
return _getUpdatedAtThreshold();
}

function getStaticResolverKey() external pure override returns (bytes32 staticResolverKey_) {
staticResolverKey_ = _RESERVE_RESOLVER_KEY;
}

function getStaticFunctionSelectors() external pure override returns (bytes4[] memory staticFunctionSelectors_) {
uint256 selectorIndex;
staticFunctionSelectors_ = new bytes4[](3);
staticFunctionSelectors_ = new bytes4[](5);
staticFunctionSelectors_[selectorIndex++] = this.getReserveAmount.selector;
staticFunctionSelectors_[selectorIndex++] = this.updateReserveAddress.selector;
staticFunctionSelectors_[selectorIndex++] = this.getReserveAddress.selector;
staticFunctionSelectors_[selectorIndex++] = this.updateUpdatedAtThreshold.selector;
staticFunctionSelectors_[selectorIndex++] = this.getUpdatedAtThreshold.selector;
}

function getStaticInterfaceIds() external pure override returns (bytes4[] memory staticInterfaceIds_) {
Expand Down
20 changes: 15 additions & 5 deletions contracts/contracts/extensions/ReserveStorageWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {_RESERVE_STORAGE_POSITION} from '../constants/storagePositions.sol';
abstract contract ReserveStorageWrapper is IReserveStorageWrapper, TokenOwnerStorageWrapper, RolesStorageWrapper {
struct ReserveStorage {
address reserveAddress;
uint256 updatedAtThreshold;
}

/**
Expand All @@ -35,8 +36,13 @@ abstract contract ReserveStorageWrapper is IReserveStorageWrapper, TokenOwnerSto
function _checkReserveAmount(uint256 amount) private view returns (bool) {
address reserveAddress = _reserveStorage().reserveAddress;
if (reserveAddress == address(0)) return true;
int256 reserveAmount = _getReserveAmount();

(int256 reserveAmount, uint256 updatedAt) = _getReserveAmount();
if (_getUpdatedAtThreshold() > 0 && updatedAt > _getUpdatedAtThreshold()) {
revert ReserveAmountOutdated(updatedAt, _getUpdatedAtThreshold());
}
assert(reserveAmount >= 0);

uint256 currentReserve = uint256(reserveAmount);
uint8 reserveDecimals = AggregatorV3Interface(reserveAddress).decimals();
uint8 tokenDecimals = _decimals();
Expand All @@ -56,13 +62,17 @@ abstract contract ReserveStorageWrapper is IReserveStorageWrapper, TokenOwnerSto
* @dev Gets the current reserve amount
*
*/
function _getReserveAmount() internal view returns (int256) {
function _getReserveAmount() internal view returns (int256, uint256) {
address reserveAddress = _reserveStorage().reserveAddress;
if (reserveAddress != address(0)) {
(, int256 answer, , , ) = AggregatorV3Interface(reserveAddress).latestRoundData();
return answer;
(, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(reserveAddress).latestRoundData();
return (answer, updatedAt);
}
return 0;
return (0, 0);
}

function _getUpdatedAtThreshold() internal view returns (uint256) {
return _reserveStorage().updatedAtThreshold;
}

function _reserveStorage() internal pure returns (ReserveStorage storage reserveStorage_) {
Expand Down
94 changes: 72 additions & 22 deletions contracts/test/thread1/reserve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { GAS_LIMIT } from '@test/shared'

let operator: SignerWithAddress
let nonOperator: SignerWithAddress
let businessLogicResolver: string
let stableCoinFactoryProxy: string

Expand All @@ -42,7 +43,7 @@ before(async () => {
// mute | mock console.log
console.log = () => {} // eslint-disable-line
console.info(MESSAGES.deploy.info.deployFullInfrastructureInTests)
;[operator] = await ethers.getSigners()
;[operator, nonOperator] = await ethers.getSigners()

const { ...deployedContracts } = await deployFullInfrastructure(
await DeployFullInfrastructureCommand.newInstance({
Expand Down Expand Up @@ -90,12 +91,35 @@ describe('➡️ Reserve Tests', function () {
})

it('Get getReserveAmount', async () => {
const reserveAmount = await reserveFacet.getReserveAmount({
const result = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const reserveAmount = result[0]
expect(reserveAmount.toString()).to.equals(INIT_RESERVE_100.toString())
})

it('Get updatedAt threshold', async () => {
const initialUpdatedAtThreshold = await reserveFacet.getUpdatedAtThreshold()
expect(initialUpdatedAtThreshold.toString()).to.equals('0')

const FINAL_THRESHOLD = 1000

await reserveFacet.updateUpdatedAtThreshold(FINAL_THRESHOLD)
await delay({ time: 1, unit: 'sec' })

const finalUpdatedAtThreshold = await reserveFacet.getUpdatedAtThreshold()
expect(finalUpdatedAtThreshold.toString()).to.equals(FINAL_THRESHOLD.toString())

await reserveFacet.updateUpdatedAtThreshold('0')
})

it('Set updatedAt threshold fails if set by non Admin', async () => {
await expect(reserveFacet.connect(nonOperator).updateUpdatedAtThreshold('1')).to.be.revertedWithCustomError(
reserveFacet,
'AccountHasNoRole'
)
})

it('Get datafeed', async () => {
const datafeed = await reserveFacet.getReserveAddress({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAddress,
Expand All @@ -104,9 +128,10 @@ describe('➡️ Reserve Tests', function () {
})

it('Update datafeed', async () => {
const beforeReserve = reserveFacet.getReserveAmount({
const beforeResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const beforeReserve = beforeResult[0]
const beforeDataFeed = reserveFacet.getReserveAddress({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAddress,
})
Expand All @@ -121,9 +146,10 @@ describe('➡️ Reserve Tests', function () {
})
await new ValidateTxResponseCommand({ txResponse: updateResponse }).execute()

const afterReserve = reserveFacet.getReserveAmount({
const afterResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const afterReserve = afterResult[0]
const afterDataFeed = reserveFacet.getReserveAddress({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAddress,
})
Expand Down Expand Up @@ -174,9 +200,10 @@ describe('Reserve Tests with reserve and token with same Decimals', function ()
const AmountToMint = 10n * TWO_TOKEN_FACTOR

// Get the initial reserve amount
const initialReserve = await reserveFacet.getReserveAmount({
const initialResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const initialReserve = initialResult[0]

// Cashin tokens to previously associated account
const mintResponse = await cashInFacet.mint(operator.address, AmountToMint, {
Expand All @@ -186,19 +213,20 @@ describe('Reserve Tests with reserve and token with same Decimals', function ()

// Check the reserve account : success
await delay({ time: 1, unit: 'sec' })
const finalReserve =
(await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})) - AmountToMint
const finalResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const finalReserve = finalResult[0] - AmountToMint
const expectedTotalReserve = initialReserve - AmountToMint
expect(finalReserve.toString()).to.equals(expectedTotalReserve.toString())
})

it('Can not mint more tokens than reserve', async function () {
// Retrieve current reserve amount
const totalReserve = await reserveFacet.getReserveAmount({
const result = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const totalReserve = result[0]

// Cashin more tokens than reserve amount: fail
const mintResponse = await cashInFacet.mint(operator.address, totalReserve + 1n, {
Expand Down Expand Up @@ -250,9 +278,10 @@ describe('Reserve Tests with reserve decimals higher than token decimals', funct
const AmountToMint = 10n * ONE_TOKEN_FACTOR

// Get the initial reserve amount
const initialReserve = await reserveFacet.getReserveAmount({
const initialResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const initialReserve = initialResult[0]

// Cashin tokens to previously associated account
const mintResponse = await cashInFacet.mint(operator.address, AmountToMint, {
Expand All @@ -262,19 +291,20 @@ describe('Reserve Tests with reserve decimals higher than token decimals', funct

// Check the reserve account : success
await delay({ time: 1, unit: 'sec' })
const finalReserve =
(await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})) - AmountToMint
const finalResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const finalReserve = finalResult[0] - AmountToMint
const expectedTotalReserve = initialReserve - AmountToMint
expect(finalReserve.toString()).to.equals(expectedTotalReserve.toString())
})

it('Can not mint more tokens than reserve', async function () {
// Retrieve current reserve amount
const totalReserve = await reserveFacet.getReserveAmount({
const result = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const totalReserve = result[0]

// Cashin more tokens than reserve amount: fail
const mintResponse = await cashInFacet.mint(operator.address, totalReserve + 1n, {
Expand Down Expand Up @@ -324,9 +354,10 @@ describe('Reserve Tests with reserve decimals lower than token decimals', functi
const AmountToMint = 10n * THREE_TOKEN_FACTOR

// Get the initial reserve amount
const initialReserve = await reserveFacet.getReserveAmount({
const initialResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const initialReserve = initialResult[0]

// Cashin tokens to previously associated account
const mintResponse = await cashInFacet.mint(operator.address, AmountToMint, {
Expand All @@ -336,24 +367,43 @@ describe('Reserve Tests with reserve decimals lower than token decimals', functi

// Check the reserve account : success
await delay({ time: 1, unit: 'sec' })
const finalReserve =
(await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})) - AmountToMint
const finalResult = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const finalReserve = finalResult[0] - AmountToMint
const expectedTotalReserve = initialReserve - AmountToMint
expect(finalReserve.toString()).to.equals(expectedTotalReserve.toString())
})

it('Can not mint more tokens than reserve', async function () {
// Retrieve current reserve amount
const totalReserve = await reserveFacet.getReserveAmount({
const result = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const totalReserve = result[0]

// Cashin more tokens than reserve amount: fail
const mintResponse = await cashInFacet.mint(operator.address, totalReserve + 1n, {
gasLimit: GAS_LIMIT.hederaTokenManager.mint,
})
await expect(new ValidateTxResponseCommand({ txResponse: mintResponse }).execute()).to.be.rejectedWith(Error)
})

it('Can not mint tokens if updated at date expired', async function () {
// Retrieve current reserve amount
const result = await reserveFacet.getReserveAmount({
gasLimit: GAS_LIMIT.hederaTokenManager.getReserveAmount,
})
const totalReserve = result[0]

await reserveFacet.updateUpdatedAtThreshold('1')
await delay({ time: 2, unit: 'sec' })

await expect(cashInFacet.mint(operator.address, totalReserve - 1n)).to.be.revertedWithCustomError(
cashInFacet,
'ReserveAmountOutdated'
)

await reserveFacet.updateUpdatedAtThreshold('0')
})
})
Loading
Loading