Skip to content

Commit

Permalink
Fix decimal issues in accountant
Browse files Browse the repository at this point in the history
  • Loading branch information
crispymangoes committed Apr 11, 2024
1 parent ddaa2fc commit 94a1abc
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 22 deletions.
44 changes: 36 additions & 8 deletions src/base/Roles/AccountantWithRateProviders.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,9 @@ contract AccountantWithRateProviders is Auth, IRateProvider {

/**
* @notice Update the rate provider data for a specific `asset`.
* @dev Rate providers must return rates in terms of `base` and
* they must use the same decimals as `base`.
* @dev Rate providers must return rates in terms of `base` or
* an asset pegged to base and they must use the same decimals
* as `asset`.
* @dev Callable by OWNER_ROLE.
*/
function setRateProviderData(ERC20 asset, bool isPeggedToBase, address rateProvider) external requiresAuth {
Expand Down Expand Up @@ -277,6 +278,8 @@ contract AccountantWithRateProviders is Auth, IRateProvider {
/**
* @notice Claim pending fees.
* @dev This function must be called by the BoringVault.
* @dev This function will lose precision if the exchange rate
* decimals is greater than the feeAsset's decimals.
*/
function claimFees(ERC20 feeAsset) external {
if (msg.sender != address(vault)) revert AccountantWithRateProviders__OnlyCallableByBoringVault();
Expand All @@ -288,11 +291,18 @@ contract AccountantWithRateProviders is Auth, IRateProvider {
// Determine amount of fees owed in feeAsset.
uint256 feesOwedInFeeAsset;
RateProviderData memory data = rateProviderData[feeAsset];
if (address(feeAsset) == address(base) || data.isPeggedToBase) {
if (address(feeAsset) == address(base)) {
feesOwedInFeeAsset = state.feesOwedInBase;
} else {
uint256 rate = data.rateProvider.getRate();
feesOwedInFeeAsset = uint256(state.feesOwedInBase).mulDivDown(rate, 10 ** decimals);
uint8 feeAssetDecimals = ERC20(feeAsset).decimals();
uint256 feesOwedInBaseUsingFeeAssetDecimals =
changeDecimals(state.feesOwedInBase, decimals, feeAssetDecimals);
if (data.isPeggedToBase) {
feesOwedInFeeAsset = feesOwedInBaseUsingFeeAssetDecimals;
} else {
uint256 rate = data.rateProvider.getRate();
feesOwedInFeeAsset = feesOwedInBaseUsingFeeAssetDecimals.mulDivDown(10 ** feeAssetDecimals, rate);
}
}
// Zero out fees owed.
state.feesOwedInBase = 0;
Expand Down Expand Up @@ -323,18 +333,22 @@ contract AccountantWithRateProviders is Auth, IRateProvider {
/**
* @notice Get this BoringVault's current rate in the provided quote.
* @dev `quote` must have its RateProviderData set, else this will revert.
* @dev This function will lose precision if the exchange rate
* decimals is greater than the quote's decimals.
*/
function getRateInQuote(ERC20 quote) public view returns (uint256 rateInQuote) {
if (address(quote) == address(base)) {
rateInQuote = accountantState.exchangeRate;
} else {
RateProviderData memory data = rateProviderData[quote];
uint8 quoteDecimals = ERC20(quote).decimals();
uint256 exchangeRateInQuoteDecimals = changeDecimals(accountantState.exchangeRate, decimals, quoteDecimals);
if (data.isPeggedToBase) {
rateInQuote = accountantState.exchangeRate;
rateInQuote = exchangeRateInQuoteDecimals;
} else {
uint256 quoteRate = data.rateProvider.getRate();
uint256 oneQuote = 10 ** ERC20(quote).decimals();
rateInQuote = oneQuote.mulDivDown(accountantState.exchangeRate, quoteRate);
uint256 oneQuote = 10 ** quoteDecimals;
rateInQuote = oneQuote.mulDivDown(exchangeRateInQuoteDecimals, quoteRate);
}
}
}
Expand All @@ -348,4 +362,18 @@ contract AccountantWithRateProviders is Auth, IRateProvider {
if (accountantState.isPaused) revert AccountantWithRateProviders__Paused();
rateInQuote = getRateInQuote(quote);
}

// ========================================= INTERNAL HELPER FUNCTIONS =========================================
/**
* @notice Used to change the decimals of precision used for an amount.
*/
function changeDecimals(uint256 amount, uint8 fromDecimals, uint8 toDecimals) internal pure returns (uint256) {
if (fromDecimals == toDecimals) {
return amount;
} else if (fromDecimals < toDecimals) {
return amount * 10 ** (toDecimals - fromDecimals);
} else {
return amount / 10 ** (fromDecimals - toDecimals);
}
}
}
182 changes: 182 additions & 0 deletions test/AccountantWithRateProvidersUsingDifferentDecimal.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import {MainnetAddresses} from "test/resources/MainnetAddresses.sol";
import {BoringVault} from "src/base/BoringVault.sol";
import {AccountantWithRateProviders} from "src/base/Roles/AccountantWithRateProviders.sol";
import {SafeTransferLib} from "@solmate/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "@solmate/utils/FixedPointMathLib.sol";
import {ERC20} from "@solmate/tokens/ERC20.sol";
import {IRateProvider} from "src/interfaces/IRateProvider.sol";
import {RolesAuthority, Authority} from "@solmate/auth/authorities/RolesAuthority.sol";

import {Test, stdStorage, StdStorage, stdError, console} from "@forge-std/Test.sol";

contract AccountantWithRateProvidersUsingDifferentDecimalTest is Test, MainnetAddresses {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
using stdStorage for StdStorage;

BoringVault public boringVault;
AccountantWithRateProviders public accountant;
address public payoutAddress = vm.addr(7777777);
RolesAuthority public rolesAuthority;

address public usdcWhale = 0x28C6c06298d514Db089934071355E5743bf21d60;

uint8 public constant MINTER_ROLE = 1;
uint8 public constant ADMIN_ROLE = 2;
uint8 public constant UPDATE_EXCHANGE_RATE_ROLE = 3;
uint8 public constant BORING_VAULT_ROLE = 4;

function setUp() external {
// Setup forked environment.
string memory rpcKey = "MAINNET_RPC_URL";
uint256 blockNumber = 19618964;
_startFork(rpcKey, blockNumber);

boringVault = new BoringVault(address(this), "Boring Vault", "BV", 6);

accountant = new AccountantWithRateProviders(
address(this), address(boringVault), payoutAddress, 1e6, address(USDC), 1.001e4, 0.999e4, 1, 0
);

vm.startPrank(usdcWhale);
USDC.safeTransfer(address(this), 1_000_000e6);
vm.stopPrank();
USDC.safeApprove(address(boringVault), 1_000_000e6);
boringVault.enter(address(this), USDC, 1_000_000e6, address(this), 1_000_000e6);

accountant.setRateProviderData(DAI, true, address(0));
accountant.setRateProviderData(USDT, true, address(0));
accountant.setRateProviderData(SDAI, false, sDaiRateProvider);

// Start accounting so we can claim fees during a test.
accountant.updateManagementFee(0.01e4);

skip(1 days / 24);
// Increase exchange rate by 5 bps.
uint96 newExchangeRate = uint96(1.0005e6);
accountant.updateExchangeRate(newExchangeRate);

skip(1 days);

accountant.updateExchangeRate(newExchangeRate);

skip(1 days);
}

function testClaimFeesUsingBase() external {
// Set exchangeRate back to 1e6.
uint96 newExchangeRate = uint96(1e6);
accountant.updateExchangeRate(newExchangeRate);

(, uint128 feesOwed,,,,,,,,) = accountant.accountantState();

vm.startPrank(address(boringVault));
USDC.safeApprove(address(accountant), type(uint256).max);
// Claim fees.
accountant.claimFees(USDC);
vm.stopPrank();

assertEq(USDC.balanceOf(payoutAddress), feesOwed, "Should have claimed fees in USDC");
}

function testClaimFeesUsingPegged() external {
// Set exchangeRate back to 1e6.
uint96 newExchangeRate = uint96(1e6);
accountant.updateExchangeRate(newExchangeRate);

(, uint128 feesOwed,,,,,,,,) = accountant.accountantState();

deal(address(USDT), address(boringVault), 1_000_000e6);
vm.startPrank(address(boringVault));
USDT.safeApprove(address(accountant), type(uint256).max);
// Claim fees.
accountant.claimFees(USDT);
vm.stopPrank();

assertEq(USDT.balanceOf(payoutAddress), feesOwed, "Should have claimed fees in USDT");
}

function testClaimFeesUsingPeggedDifferentDecimals() external {
// Set exchangeRate back to 1e6.
uint96 newExchangeRate = uint96(1e6);
accountant.updateExchangeRate(newExchangeRate);

(, uint128 feesOwed,,,,,,,,) = accountant.accountantState();

deal(address(DAI), address(boringVault), 1_000_000e18);
vm.startPrank(address(boringVault));
DAI.safeApprove(address(accountant), type(uint256).max);
// Claim fees.
accountant.claimFees(DAI);
vm.stopPrank();

uint256 expectedFeesOwed = uint256(feesOwed).mulDivDown(1e18, 1e6);
assertEq(DAI.balanceOf(payoutAddress), expectedFeesOwed, "Should have claimed fees in DAI");
}

function testClaimFeesUsingRateProviderAsset() external {
// Set exchangeRate back to 1e6.
uint96 newExchangeRate = uint96(1e6);
accountant.updateExchangeRate(newExchangeRate);

(, uint128 feesOwed,,,,,,,,) = accountant.accountantState();

deal(address(SDAI), address(boringVault), 1_000_000e18);
vm.startPrank(address(boringVault));
SDAI.safeApprove(address(accountant), type(uint256).max);
// Claim fees.
accountant.claimFees(SDAI);
vm.stopPrank();

uint256 expectedFeesOwed = uint256(feesOwed).mulDivDown(1e18, 1e6);
expectedFeesOwed = expectedFeesOwed.mulDivDown(1e18, IRateProvider(sDaiRateProvider).getRate());
uint256 sDaiFees = SDAI.balanceOf(payoutAddress);
assertEq(sDaiFees, expectedFeesOwed, "Should have claimed fees in SDAI");

// Convert fees received to USDC.
uint256 feesConvertedToUsdc = sDaiFees.mulDivDown(IRateProvider(sDaiRateProvider).getRate(), 1e18);
feesConvertedToUsdc = feesConvertedToUsdc.mulDivDown(1e6, 1e18);
assertApproxEqAbs(
feesOwed, feesConvertedToUsdc, 1, "sDAI fees converted to USDC should be equal to fees owed in USDC"
);
}

function testRates() external {
// Set exchangeRate back to 1e6.
uint96 newExchangeRate = uint96(1e6);
accountant.updateExchangeRate(newExchangeRate);

// getRate and getRate in quote should work.
uint256 rate = accountant.getRate();
uint256 expected_rate = 1e6;
assertEq(rate, expected_rate, "Rate should be expected rate");
rate = accountant.getRateSafe();
assertEq(rate, expected_rate, "Rate should be expected rate");

uint256 rateInQuote = accountant.getRateInQuote(USDC);
expected_rate = 1e6;
assertEq(rateInQuote, expected_rate, "Rate should be expected rate");

rateInQuote = accountant.getRateInQuote(DAI);
expected_rate = 1e18;
assertEq(rateInQuote, expected_rate, "Rate should be expected rate");

rateInQuote = accountant.getRateInQuote(USDT);
expected_rate = 1e6;
assertEq(rateInQuote, expected_rate, "Rate should be expected rate");

rateInQuote = accountant.getRateInQuote(SDAI);
expected_rate = uint256(1e18).mulDivDown(1e18, IRateProvider(sDaiRateProvider).getRate());
assertEq(rateInQuote, expected_rate, "Rate should be expected rate for sDAI");
}

// ========================================= HELPER FUNCTIONS =========================================

function _startFork(string memory rpcKey, uint256 blockNumber) internal returns (uint256 forkId) {
forkId = vm.createFork(vm.envString(rpcKey), blockNumber);
vm.selectFork(forkId);
}
}
56 changes: 42 additions & 14 deletions test/resources/MainnetAddresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,10 @@ pragma solidity 0.8.21;
import {ERC20} from "@solmate/tokens/ERC20.sol";

contract MainnetAddresses {
// Sommelier
address public gravityBridgeAddress = 0x69592e6f9d21989a043646fE8225da2600e5A0f7;
// address public strategist = 0xA9962a5BfBea6918E958DeE0647E99fD7863b95A;
address public devStrategist = 0xeeF7b7205CAF2Bcd71437D9acDE3874C3388c138;
address public cosmos = address(0xCAAA);
// address public multisig = 0x7340D1FeCD4B64A4ac34f826B21c945d44d7407F;
address public ryusdRegistry = 0x2Cbd27E034FEE53f79b607430dA7771B22050741;
address public ryusdRegistryOwner = 0xaDa78a5E01325B91Bc7879a63c309F7D54d42950;
address public deployerAddress = 0xdAFAe2FfB48F1b5b710DD71FBaf8E6C7a67aBF89;
address public dev0Address = 0x552acA1343A6383aF32ce1B7c7B1b47959F7ad90;

address public priceRouterV1 = 0x138a6d8c49428D4c71dD7596571fbd4699C7D3DA;
address public priceRouterV2 = 0xA1A0bc3D59e4ee5840c9530e49Bdc2d1f88AaF92;
address public ryusdAddress = 0x97e6E0a40a3D02F12d1cEC30ebfbAE04e37C119E;
// Liquid Ecosystem
address public deployerAddress;
address public dev0Address = 0x0463E60C7cE10e57911AB7bD1667eaa21de3e79b;
address public dev1Address = 0x2322ba43eFF1542b6A7bAeD35e66099Ea0d12Bd1;

// DeFi Ecosystem
address public ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
Expand Down Expand Up @@ -66,6 +56,10 @@ contract MainnetAddresses {
ERC20 public OSETH = ERC20(0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38);
ERC20 public RSWETH = ERC20(0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0);
ERC20 public PENDLE = ERC20(0x808507121B80c02388fAd14726482e061B8da827);
ERC20 public SUSDE = ERC20(0x9D39A5DE30e57443BfF2A8307A4256c8797A3497);
ERC20 public USDE = ERC20(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3);
ERC20 public GEAR = ERC20(0xBa3335588D9403515223F109EdC4eB7269a9Ab5D);
ERC20 public SDAI = ERC20(0x83F20F44975D03b1b09e64809B757c47f942BEeA);

// Rate providers
address public WEETH_RATE_PROVIDER = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee;
Expand Down Expand Up @@ -191,6 +185,7 @@ contract MainnetAddresses {
// Rate Providers
address public cbethRateProvider = 0x7311E4BB8a72e7B300c5B8BDE4de6CdaA822a5b1;
address public rethRateProvider = 0x1a8F81c256aee9C640e14bB0453ce247ea0DFE6F;
address public sDaiRateProvider = 0xc7177B6E18c1Abd725F5b75792e5F7A3bA5DBC2c;

// Compound V2
// CErc20 public cDAI = CErc20(0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643);
Expand Down Expand Up @@ -298,6 +293,9 @@ contract MainnetAddresses {
address public ezETH_wETH_Curve_Pool = 0x85dE3ADd465a219EE25E04d22c39aB027cF5C12E;
address public weETH_rswETH_Curve_Pool = 0x278cfB6f06B1EFc09d34fC7127d6060C61d629Db;
address public rswETH_wETH_Curve_Pool = 0xeE04382c4cA6c450213923fE0f0daB19b0ff3939;
address public USDe_USDC_Curve_Pool = 0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72;
address public USDe_DAI_Curve_Pool = 0xF36a4BA50C603204c3FC6d2dA8b78A7b69CBC67d;
address public sDAI_sUSDe_Curve_Pool = 0x167478921b907422F8E88B43C4Af2B8BEa278d3A;

address public WethMkUsdPool = 0xc89570207c5BA1B0E3cD372172cCaEFB173DB270;

Expand Down Expand Up @@ -395,6 +393,12 @@ contract MainnetAddresses {
// Gearbox
address public dWETHV3 = 0xda0002859B2d05F66a753d8241fCDE8623f26F4f;
address public sdWETHV3 = 0x0418fEB7d0B25C411EB77cD654305d29FcbFf685;
address public dUSDCV3 = 0xda00000035fef4082F78dEF6A8903bee419FbF8E;
address public sdUSDCV3 = 0x9ef444a6d7F4A5adcd68FD5329aA5240C90E14d2;
address public dDAIV3 = 0xe7146F53dBcae9D6Fa3555FE502648deb0B2F823;
address public sdDAIV3 = 0xC853E4DA38d9Bd1d01675355b8c8f3BBC1451973;
address public dUSDTV3 = 0x05A811275fE9b4DE503B3311F51edF6A856D936e;
address public sdUSDTV3 = 0x16adAb68bDEcE3089D4f1626Bb5AEDD0d02471aD;

// Pendle
address public pendleMarketFactory = 0x1A6fCc85557BC4fB7B534ed835a03EF056552D52;
Expand All @@ -411,11 +415,24 @@ contract MainnetAddresses {
address public pendleEzEthPt = 0xeEE8aED1957ca1545a0508AfB51b53cCA7e3c0d1;
address public pendleEzEthYt = 0x256Fb830945141f7927785c06b65dAbc3744213c;

address public pendleUSDeMarket = 0xb4460e76D99eCaD95030204D3C25fb33C4833997;
address public pendleUSDeSy = 0x248d83f7Ac03CEbCC06d5C1A52c9B5d0EbB9936e;
address public pendleUSDePt = 0x0869055C098Fe927E1816CAAfb16E9133e5258FE;
address public pendleUSDeYt = 0xA99A4329c49369fB41e6CB6852535Bbf3d4f749b;

address public pendleSUSDeMarket = 0x8f7627bD46B30E296Aa3AAbe1dF9Bfac10920B6E;
address public pendleSUSDeSy = 0x22E12A50e3ca49FB183074235cB1db84Fe4C716D;
address public pendleSUSDePt = 0x215a61deaD7e1303b8571Bb429491aeAc5f2061e;
address public pendleSUSDeYt = 0x20c8Cf9714339fd018898cB5De84c089e48d1c87;

address public pendleSwethMarket = 0x0e1C5509B503358eA1Dac119C1D413e28Cc4b303;

// Aave V3
address public v3Pool = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;

// SparkLend
address public sparkLendPool = 0xC13e21B648A5Ee794902342038FF3aDAB66BE987;

// Uniswap V3 Pools
address public wETH_weETH_05 = 0x7A415B19932c0105c82FDB6b720bb01B0CC2CAe3;
address public wstETH_wETH_01 = 0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa;
Expand All @@ -426,4 +443,15 @@ contract MainnetAddresses {
address public wETH_rswETH_30 = 0xE62627326d7794E20bB7261B24985294de1579FE;
address public ezETH_wETH_01 = 0xBE80225f09645f172B079394312220637C440A63;
address public PENDLE_wETH_30 = 0x57aF956d3E2cCa3B86f3D8C6772C03ddca3eAacB;
address public USDe_USDT_01 = 0x435664008F38B0650fBC1C9fc971D0A3Bc2f1e47;
address public USDe_USDC_01 = 0xE6D7EbB9f1a9519dc06D557e03C522d53520e76A;
address public USDe_DAI_01 = 0x5B3a0f1acBE8594a079FaFeB1c84DEA9372A5Aad;
address public sUSDe_USDT_05 = 0x867B321132B18B5BF3775c0D9040D1872979422E;
address public GEAR_wETH_100 = 0xaEf52f72583E6c4478B220Da82321a6a023eEE50;
address public GEAR_USDT_30 = 0x349eE001D80f896F24571616932f54cBD66B18C9;
address public DAI_USDC_01 = 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168;
address public DAI_USDC_05 = 0x6c6Bc977E13Df9b0de53b251522280BB72383700;
address public USDC_USDT_01 = 0x3416cF6C708Da44DB2624D63ea0AAef7113527C6;
address public USDC_USDT_05 = 0x7858E59e0C01EA06Df3aF3D20aC7B0003275D4Bf;
address public USDC_wETH_05 = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640;
}

0 comments on commit 94a1abc

Please sign in to comment.