diff --git a/contracts/protocol/libraries/math/RayMathExplicitRounding.sol b/contracts/protocol/libraries/math/RayMathExplicitRounding.sol new file mode 100644 index 000000000..8d3f3dcb2 --- /dev/null +++ b/contracts/protocol/libraries/math/RayMathExplicitRounding.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.10; + +enum Rounding { + UP, + DOWN +} + +/** + * Simplified version of RayMath that instead of half-up rounding does explicit rounding in a specified direction. + * This is needed to have a 4626 complient implementation, that always predictable rounds in favor of the vault / static a token. + */ +library RayMathExplicitRounding { + uint256 internal constant RAY = 1e27; + uint256 internal constant WAD_RAY_RATIO = 1e9; + + function rayMulRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0 || b == 0) { + return 0; + } + return (a * b) / RAY; + } + + function rayMulRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0 || b == 0) { + return 0; + } + return ((a * b) + RAY - 1) / RAY; + } + + function rayDivRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { + return (a * RAY) / b; + } + + function rayDivRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + return ((a * RAY) + b - 1) / b; + } + + function rayToWadRoundDown(uint256 a) internal pure returns (uint256) { + return a / WAD_RAY_RATIO; + } +} diff --git a/contracts/protocol/tokenization/AToken.sol b/contracts/protocol/tokenization/AToken.sol index 57f3b167f..7ab11dfb2 100644 --- a/contracts/protocol/tokenization/AToken.sol +++ b/contracts/protocol/tokenization/AToken.sol @@ -7,6 +7,7 @@ import {SafeCast} from '../../dependencies/openzeppelin/contracts/SafeCast.sol'; import {VersionedInitializable} from '../libraries/aave-upgradeability/VersionedInitializable.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; +import {RayMathExplicitRounding, Rounding} from '../libraries/math/RayMathExplicitRounding.sol'; import {IPool} from '../../interfaces/IPool.sol'; import {IAToken} from '../../interfaces/IAToken.sol'; import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; @@ -22,6 +23,7 @@ import {EIP712Base} from './base/EIP712Base.sol'; */ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, IAToken { using WadRayMath for uint256; + using RayMathExplicitRounding for uint256; using SafeCast for uint256; using GPv2SafeERC20 for IERC20; @@ -89,7 +91,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I uint256 amount, uint256 index ) external virtual override onlyPool returns (bool) { - return _mintScaled(caller, onBehalfOf, amount, index); + return _mintScaled(caller, onBehalfOf, amount, index, Rounding.DOWN); } /// @inheritdoc IAToken @@ -99,7 +101,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I uint256 amount, uint256 index ) external virtual override onlyPool { - _burnScaled(from, receiverOfUnderlying, amount, index); + _burnScaled(from, receiverOfUnderlying, amount, index, Rounding.UP); if (receiverOfUnderlying != address(this)) { IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); } @@ -110,7 +112,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I if (amount == 0) { return; } - _mintScaled(address(POOL), _treasury, amount, index); + _mintScaled(address(POOL), _treasury, amount, index, Rounding.DOWN); } /// @inheritdoc IAToken @@ -128,7 +130,7 @@ contract AToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, I function balanceOf( address user ) public view virtual override(IncentivizedERC20, IERC20) returns (uint256) { - return super.balanceOf(user).rayMul(POOL.getReserveNormalizedIncome(_underlyingAsset)); + return super.balanceOf(user).rayMulRoundDown(POOL.getReserveNormalizedIncome(_underlyingAsset)); } /// @inheritdoc IERC20 diff --git a/contracts/protocol/tokenization/VariableDebtToken.sol b/contracts/protocol/tokenization/VariableDebtToken.sol index 902963a59..e82a254a4 100644 --- a/contracts/protocol/tokenization/VariableDebtToken.sol +++ b/contracts/protocol/tokenization/VariableDebtToken.sol @@ -5,6 +5,7 @@ import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; import {SafeCast} from '../../dependencies/openzeppelin/contracts/SafeCast.sol'; import {VersionedInitializable} from '../libraries/aave-upgradeability/VersionedInitializable.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; +import {RayMathExplicitRounding, Rounding} from '../libraries/math/RayMathExplicitRounding.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; import {IPool} from '../../interfaces/IPool.sol'; import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; @@ -23,6 +24,7 @@ import {ScaledBalanceTokenBase} from './base/ScaledBalanceTokenBase.sol'; */ contract VariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IVariableDebtToken { using WadRayMath for uint256; + using RayMathExplicitRounding for uint256; using SafeCast for uint256; uint256 public constant DEBT_TOKEN_REVISION = 0x1; @@ -84,7 +86,7 @@ contract VariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IVariableDe return 0; } - return scaledBalance.rayMul(POOL.getReserveNormalizedVariableDebt(_underlyingAsset)); + return scaledBalance.rayMulRoundUp(POOL.getReserveNormalizedVariableDebt(_underlyingAsset)); } /// @inheritdoc IVariableDebtToken @@ -97,7 +99,7 @@ contract VariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IVariableDe if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } - return (_mintScaled(user, onBehalfOf, amount, index), scaledTotalSupply()); + return (_mintScaled(user, onBehalfOf, amount, index, Rounding.UP), scaledTotalSupply()); } /// @inheritdoc IVariableDebtToken @@ -106,7 +108,7 @@ contract VariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IVariableDe uint256 amount, uint256 index ) external virtual override onlyPool returns (uint256) { - _burnScaled(from, address(0), amount, index); + _burnScaled(from, address(0), amount, index, Rounding.DOWN); return scaledTotalSupply(); } diff --git a/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol b/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol index d0010e5f5..4afa9c2ee 100644 --- a/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol +++ b/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.10; import {SafeCast} from '../../../dependencies/openzeppelin/contracts/SafeCast.sol'; import {Errors} from '../../libraries/helpers/Errors.sol'; import {WadRayMath} from '../../libraries/math/WadRayMath.sol'; +import {RayMathExplicitRounding, Rounding} from '../../libraries/math/RayMathExplicitRounding.sol'; import {IPool} from '../../../interfaces/IPool.sol'; import {IScaledBalanceToken} from '../../../interfaces/IScaledBalanceToken.sol'; import {MintableIncentivizedERC20} from './MintableIncentivizedERC20.sol'; @@ -15,6 +16,7 @@ import {MintableIncentivizedERC20} from './MintableIncentivizedERC20.sol'; */ abstract contract ScaledBalanceTokenBase is MintableIncentivizedERC20, IScaledBalanceToken { using WadRayMath for uint256; + using RayMathExplicitRounding for uint256; using SafeCast for uint256; /** @@ -61,15 +63,22 @@ abstract contract ScaledBalanceTokenBase is MintableIncentivizedERC20, IScaledBa * @param onBehalfOf The address of the user that will receive the scaled tokens * @param amount The amount of tokens getting minted * @param index The next liquidity index of the reserve + * @param rounding Rounding up or down when calculating scaled amount * @return `true` if the the previous balance of the user was 0 */ function _mintScaled( address caller, address onBehalfOf, uint256 amount, - uint256 index + uint256 index, + Rounding rounding ) internal returns (bool) { - uint256 amountScaled = amount.rayDiv(index); + uint256 amountScaled; + if (rounding == Rounding.UP) { + amountScaled = amount.rayDivRoundUp(index); + } else { + amountScaled = amount.rayDivRoundDown(index); + } require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT); uint256 scaledBalance = super.balanceOf(onBehalfOf); @@ -95,9 +104,21 @@ abstract contract ScaledBalanceTokenBase is MintableIncentivizedERC20, IScaledBa * @param target The address that will receive the underlying, if any * @param amount The amount getting burned * @param index The variable debt index of the reserve + * @param rounding Rounding up or down when calculating scaled amount */ - function _burnScaled(address user, address target, uint256 amount, uint256 index) internal { - uint256 amountScaled = amount.rayDiv(index); + function _burnScaled( + address user, + address target, + uint256 amount, + uint256 index, + Rounding rounding + ) internal { + uint256 amountScaled; + if (rounding == Rounding.UP) { + amountScaled = amount.rayDivRoundUp(index); + } else { + amountScaled = amount.rayDivRoundDown(index); + } require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT); uint256 scaledBalance = super.balanceOf(user);