diff --git a/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol b/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol new file mode 100644 index 00000000000..a3a685014ec --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/CeloFeeCurrencyAdapterOwnable.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "./FeeCurrencyAdapterOwnable.sol"; + +contract CeloFeeCurrencyAdapterOwnable is FeeCurrencyAdapterOwnable { + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) FeeCurrencyAdapterOwnable(test) {} + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol new file mode 100644 index 00000000000..102076f093c --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +import "../../contracts/common/CalledByVm.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/stability/interfaces/ISortedOracles.sol"; +import "./interfaces/IFeeCurrency.sol"; +import "./interfaces/IDecimals.sol"; +import "./interfaces/IFeeCurrencyAdapter.sol"; + +contract FeeCurrencyAdapter is Initializable, CalledByVm, IFeeCurrencyAdapter { + IFeeCurrency public adaptedToken; + + uint96 public digitDifference; + + uint256 public debited = 0; + + string public name; + string public symbol; + + uint8 public expectedDecimals; + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param _adaptedToken The address of the adapted token. + * @param _name The name of the adapted token. + * @param _symbol The symbol of the adapted token. + * @param _expectedDecimals The expected number of decimals of the adapted token. + */ + function initialize( + address _adaptedToken, + string memory _name, + string memory _symbol, + uint8 _expectedDecimals + ) public virtual initializer { + _setAdaptedToken(_adaptedToken); + name = _name; + symbol = _symbol; + uint8 _decimals = IDecimals(_adaptedToken).decimals(); + require( + _decimals < _expectedDecimals, + "Decimals of adapted token must be < expected decimals." + ); + digitDifference = uint96(10**(_expectedDecimals - _decimals)); + expectedDecimals = _expectedDecimals; + } + + /** + * Downscales value to the adapted token's native digits and debits it. + * @param from from address + * @param value Debited value in the adapted digits. + */ + function debitGasFees(address from, uint256 value) external onlyVm { + uint256 valueScaled = downscale(value); + debited = valueScaled; + adaptedToken.debitGasFees(from, valueScaled); + } + + /** + * Downscales value to the adapted token's native digits and credits it. + * @param refundRecipient The recipient of the refund. + * @param tipRecipient The recipient of the tip. + * @param _gatewayFeeRecipient The recipient of the gateway fee. Unused. + * @param baseFeeRecipient The recipient of the base fee. + * @param refundAmount The amount to refund (in adapted token digits). + * @param tipAmount The amount to tip (in adapted token digits). + * @param _gatewayFeeAmount The amount of the gateway fee (in adapted token digits). Unused. + * @param baseFeeAmount The amount of the base fee (in adapted token digits). + */ + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external onlyVm { + if (debited == 0) { + // When eth.estimateGas is called, this function is called but we don't want to credit anything. + return; + } + + uint256 refundScaled = downscale(refundAmount); + uint256 tipTxFeeScaled = downscale(tipAmount); + uint256 baseTxFeeScaled = downscale(baseFeeAmount); + + require( + refundScaled + tipTxFeeScaled + baseTxFeeScaled <= debited, + "Cannot credit more than debited." + ); + + uint256 roundingError = debited - (refundScaled + tipTxFeeScaled + baseTxFeeScaled); + + if (roundingError > 0) { + baseTxFeeScaled += roundingError; + } + adaptedToken.creditGasFees( + refundRecipient, + tipRecipient, + address(0), + baseFeeRecipient, + refundScaled, + tipTxFeeScaled, + 0, + baseTxFeeScaled + ); + + debited = 0; + } + + /** + * @notice Returns adapted token address. + * @return The adapted token address. + */ + function getAdaptedToken() external view returns (address) { + return address(adaptedToken); + } + + /** + * @notice Gets the balance of the specified address with correct digits. + * @param account The address to query the balance of. + * @return The balance of the specified address. + */ + function balanceOf(address account) external view returns (uint256) { + return upscale(adaptedToken.balanceOf(account)); + } + + /** + * @notice Gets the total supply with correct digits. + * @return The total supply. + */ + function totalSupply() external view returns (uint256) { + return upscale(adaptedToken.totalSupply()); + } + + /** + * @notice Gets the total supply with correct digits. + * @return The total supply. + */ + function decimals() external view returns (uint8) { + return expectedDecimals; + } + + function upscale(uint256 value) internal view returns (uint256) { + return value * digitDifference; + } + + function downscale(uint256 value) internal view returns (uint256) { + return value / digitDifference; + } + + function _setAdaptedToken(address _adaptedToken) internal virtual { + adaptedToken = IFeeCurrency(_adaptedToken); + } +} diff --git a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol new file mode 100644 index 00000000000..e93ab8ddcd3 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapterOwnable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +import "./FeeCurrencyAdapter.sol"; + +contract FeeCurrencyAdapterOwnable is FeeCurrencyAdapter, Ownable { + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) FeeCurrencyAdapter(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param _adaptedToken The address of the adapted token. + * @param _name The name of the adapted token. + * @param _symbol The symbol of the adapted token. + * @param _expectedDecimals The expected number of decimals of the adapted token. + */ + function initialize( + address _adaptedToken, + string memory _name, + string memory _symbol, + uint8 _expectedDecimals + ) public override { + _transferOwnership(msg.sender); + super.initialize(_adaptedToken, _name, _symbol, _expectedDecimals); + } + + /** + * @notice Sets adapted token address. + * @param _adaptedToken The address of the adapted token. + */ + function setAdaptedToken(address _adaptedToken) public onlyOwner { + _setAdaptedToken(_adaptedToken); + } +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol new file mode 100644 index 00000000000..2b68b9f8060 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.13; + +interface IDecimals { + function decimals() external view returns (uint8); +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol new file mode 100644 index 00000000000..e8c800a3c93 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol @@ -0,0 +1,58 @@ +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +interface IFeeCurrency is IERC20 { + /* + This interface should be implemented for tokens which are supposed to + act as fee currencies on the Celo blockchain, meaning that they can be + used to pay gas fees for CIP-64 transactions (and some older tx types). + See https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md + + Before executing a tx with non-empty feeCurrency field, the fee + currency's `debitGasFees` function is called to reserve the maximum + amount that tx can spend on gas. After the tx has been executed, the + `creditGasFees` function is called to refund the unused gas and credit + the spent fees to the correct recipients. Events which are raised inside + these functions will show up for every transaction using the token as a + fee currency. + + Requirements: + - The functions will be called by the blockchain client with `msg.sender + == address(0)`. If this condition is not met, the functions must + revert to prevent malicious users from crediting their accounts directly. + - `creditGasFees` must credit all specified amounts. If it impossible to + credit one of the recipients for some reason, add the amount to the + value credited to the first valid recipient. This is important to keep + the debited and credited amounts consistent. + */ + + // Called before transaction execution to reserve the maximum amount of gas + // that can be used by the transaction. + // - The implementation must reduce `from`'s balance by `value`. + // - Must revert if `msg.sender` is not the zero address. + function debitGasFees(address from, uint256 value) external; + + /** + * Called after transaction execution to refund the unused gas and credit the + * spent fees to the correct recipients. + * @param refundRecipient The recipient of the refund. + * @param tipRecipient The recipient of the tip. + * @param _gatewayFeeRecipient The recipient of the gateway fee. Unused. + * @param baseFeeRecipient The recipient of the base fee. + * @param refundAmount The amount to refund. + * @param tipAmount The amount to tip. + * @param _gatewayFeeAmount The amount of the gateway fee. Unused. + * @param baseFeeAmount The amount of the base fee. + */ + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external; +} diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol new file mode 100644 index 00000000000..61358d38af5 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrencyAdapter.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +interface IFeeCurrencyAdapter { + function getAdaptedToken() external view returns (address); + + function digitDifference() external view returns (uint96); + + function debited() external view returns (uint256); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function expectedDecimals() external view returns (uint8); + + function decimals() external view returns (uint8); + + function debitGasFees(address from, uint256 value) external; + + function creditGasFees( + address refundRecipient, + address tipRecipient, + address _gatewayFeeRecipient, + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 _gatewayFeeAmount, + uint256 baseFeeAmount + ) external; +} diff --git a/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol b/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol new file mode 100644 index 00000000000..8141d95d0f2 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/FeeCurrencyAdapterProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract FeeCurrencyAdapterProxy is Proxy {} diff --git a/packages/protocol/foundry.toml b/packages/protocol/foundry.toml index 6347022e32a..a8c17db352d 100644 --- a/packages/protocol/foundry.toml +++ b/packages/protocol/foundry.toml @@ -4,15 +4,17 @@ out = 'out' test = 'test-sol' libs = ['lib', 'node_modules'] remappings = [ - 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', - 'forge-std/=lib/celo-foundry/lib/forge-std/src/', - 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', - 'openzeppelin-solidity/=lib/openzeppelin-contracts/', - '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', - 'celo-foundry/=lib/celo-foundry/src/', - 'celo-foundry-8/=lib/celo-foundry-8/src/', - 'solidity-bytes-utils/=lib/solidity-bytes-utils/', - '@summa-tx/memview.sol/=lib/memview.sol', + 'openzeppelin-solidity/=lib/openzeppelin-contracts/', + 'solidity-bytes-utils/=lib/solidity-bytes-utils/', + 'forge-std/=lib/celo-foundry/lib/forge-std/src/', + 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', + 'celo-foundry/=lib/celo-foundry/src/', + '@summa-tx/memview.sol/=lib/memview.sol', + 'celo-foundry-8/=lib/celo-foundry-8/src/', + 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', + '@celo-contracts-8=contracts-0.8/', + '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', + '@celo-contracts=contracts/' ] no_match_contract = "RandomTest" diff --git a/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol b/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol new file mode 100644 index 00000000000..e5131b1edc5 --- /dev/null +++ b/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: apache-2.0 +pragma solidity >=0.8.7 <=0.8.20; + +import "celo-foundry-8/Test.sol"; + +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/common/interfaces/IRegistry.sol"; + +// Contract to test +import "@celo-contracts-8/stability/CeloFeeCurrencyAdapterOwnable.sol"; +import "@celo-contracts-8/stability/interfaces/IFeeCurrency.sol"; +import "@openzeppelin/contracts8/token/ERC20/ERC20.sol"; +import "forge-std/console.sol"; + +contract FeeCurrency6DecimalsTest is ERC20, IFeeCurrency { + uint256 debited; + + constructor(uint256 initialSupply) ERC20("ExampleFeeCurrency", "EFC") { + _mint(msg.sender, initialSupply); + } + + function debitGasFees(address from, uint256 value) external { + _burn(from, value); + debited = value; + } + + // New function signature, will be used when all fee currencies have migrated + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) public { + require(recipients.length == amounts.length, "Recipients and amounts must be the same length."); + + uint256 totalSum = 0; + + for (uint256 i = 0; i < recipients.length; i++) { + _mint(recipients[i], amounts[i]); + totalSum += amounts[i]; + } + + require(debited == totalSum, "Cannot credit more than debited."); + debited = 0; + } + + // Old function signature for backwards compatibility + function creditGasFees( + address from, + address feeRecipient, + address, // gatewayFeeRecipient, unused + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256, // gatewayFee, unused + uint256 baseTxFee + ) public { + require(debited == refund + tipTxFee + baseTxFee, "Cannot credit more than debited."); + // Calling the new creditGasFees would make sense here, but that is not + // possible due to its calldata arguments. + _mint(from, refund); + _mint(feeRecipient, tipTxFee); + _mint(communityFund, baseTxFee); + + debited = 0; + } + + function decimals() public pure override returns (uint8) { + return 6; + } +} + +contract CeloFeeCurrencyAdapterTestContract is CeloFeeCurrencyAdapterOwnable { + constructor(bool test) CeloFeeCurrencyAdapterOwnable(test) {} + + function upscaleVisible(uint256 value) external view returns (uint256) { + return upscale(value); + } + + function downscaleVisible(uint256 value) external view returns (uint256) { + return downscale(value); + } +} + +contract FeeCurrencyAdapterTest is Test { + using FixidityLib for FixidityLib.Fraction; + + event GasFeesDebited(address indexed debitedFrom, uint256 debitedAmount); + + event GasFeesCredited( + address indexed refundRecipient, + address indexed tipRecipient, + address indexed baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256 baseFeeAmount + ); + + CeloFeeCurrencyAdapterTestContract public feeCurrencyAdapter; + CeloFeeCurrencyAdapterTestContract public feeCurrencyAdapterForFuzzyTests; + address owner; + address nonOwner; + IFeeCurrency feeCurrency; + + uint256 initialSupply = 10_000; + + function setUp() public virtual { + owner = address(this); + nonOwner = actor("nonOwner"); + + feeCurrencyAdapter = new CeloFeeCurrencyAdapterTestContract(true); + feeCurrencyAdapterForFuzzyTests = new CeloFeeCurrencyAdapterTestContract(true); + + address feeCurrencyAddress = actor("feeCurrency"); + + string memory name = "tokenName"; + string memory symbol = "tN"; + + feeCurrency = new FeeCurrency6DecimalsTest(initialSupply); + + feeCurrencyAdapter.initialize(address(feeCurrency), "wrapper", "wr", 18); + } +} + +contract FeeCurrencyAdapter_Initialize is FeeCurrencyAdapterTest { + function test_ShouldSetDigitDifference() public { + assertEq(feeCurrencyAdapter.digitDifference(), 10**12); + } + + function test_shouldRevertWhenCalledAgain() public { + vm.expectRevert("contract already initialized"); + feeCurrencyAdapter.initialize(address(feeCurrency), "adapter", "ad", 18); + } + + function test_ShouldSucceed_WhenExpectedDecimalsAreMoreThenDecimals_Fuzz(uint8 amount) public { + vm.assume(amount > 6); + vm.assume(amount < 50); + console.log("amount", amount); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", amount); + } + + function test_ShouldRevert_WhenExpectedDecimalsAreLessThenDecimals() public { + vm.expectRevert("Decimals of adapted token must be < expected decimals."); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", 5); + } + + function test_ShouldRevert_WhenExpectedDecimalsAreEqualToDecimals() public { + vm.expectRevert("Decimals of adapted token must be < expected decimals."); + feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", 6); + } +} + +contract FeeCurrencyAdapter_BalanceOf is FeeCurrencyAdapterTest { + function test_shouldReturnBalanceOf() public { + assertEq(feeCurrency.balanceOf(address(this)), initialSupply); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), initialSupply * 1e12); + } +} + +contract FeeCurrencyAdapter_TotalSupply is FeeCurrencyAdapterTest { + function test_shouldReturnTotalSupply() public { + assertEq(feeCurrency.totalSupply(), initialSupply); + assertEq(feeCurrencyAdapter.totalSupply(), initialSupply * 1e12); + } +} + +contract FeeCurrencyAdapter_Decimals is FeeCurrencyAdapterTest { + function test_shouldReturnDecimals() public { + assertEq(feeCurrencyAdapter.decimals(), 18); + } +} + +contract FeeCurrencyAdapter_DebitGasFees is FeeCurrencyAdapterTest { + function test_shouldDebitGasFees() public { + uint256 amount = 1000 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply - amount / 1e12); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), (initialSupply * 1e12 - amount)); + assertEq(feeCurrencyAdapter.debited(), amount / 1e12); + } + + function test_shouldRevert_WhenNotCalledByVm() public { + vm.expectRevert("Only VM can call"); + feeCurrencyAdapter.debitGasFees(address(this), 1000); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsOnlyOneBigger() public { + debitFuzzyHelper(7, 1e1); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsBigger() public { + debitFuzzyHelper(10, 1e4); + } + + function test_ShouldDebitCorrectAmount_WhenExpectedDigitsALotBigger() public { + debitFuzzyHelper(30, 1e24); + } + + function debitFuzzyHelper(uint8 expectedDigits, uint256 multiplier) public { + feeCurrencyAdapterForFuzzyTests.initialize( + address(feeCurrency), + "adapter", + "ad", + expectedDigits + ); + uint256 amount = 1000 * multiplier; + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.debitGasFees(address(this), amount); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply - amount / multiplier); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(address(this)), + (initialSupply * multiplier - amount) + ); + assertEq(feeCurrencyAdapterForFuzzyTests.debited(), amount / multiplier); + } +} + +contract FeeCurrencyAdapter_CreditGasFees is FeeCurrencyAdapterTest { + function test_shouldCreditGasFees() public { + uint256 amount = 1000 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(0), + address(this), + amount / 4, + amount / 4, + 0, + amount / 4 + ); + assertEq(feeCurrency.balanceOf(address(this)), initialSupply); + assertEq(feeCurrencyAdapter.balanceOf(address(this)), initialSupply * 1e12); + } + + function test_shouldRevert_WhenTryingToCreditMoreThanBurned() public { + uint256 amount = 1 * 1e12; + vm.prank(address(0)); + feeCurrencyAdapter.debitGasFees(address(this), amount); + + vm.expectRevert("Cannot credit more than debited."); + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1 ether, + 1 ether, + 1 ether, + 1 ether + ); + } + + function test_shouldRevert_WhenNotCalledByVm() public { + vm.expectRevert("Only VM can call"); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1000, + 1000, + 1000, + 1000 + ); + } + + function test_shouldNotRunFunctionBody_WhenDebitedIs0() public { + uint256 balanceBefore = feeCurrency.balanceOf(address(this)); + vm.prank(address(0)); + feeCurrencyAdapter.creditGasFees( + address(this), + address(this), + address(this), + address(this), + 1000, + 1000, + 1000, + 1000 + ); + uint256 balanceAfter = feeCurrency.balanceOf(address(this)); + assertEq(balanceBefore, balanceAfter); + } + + function test_shouldCreditGasFees_WhenOnlyOneBigger() public { + creditFuzzHelper(7, 1e1); + } + + function test_shouldCreditGasFees_WhenBigger() public { + creditFuzzHelper(10, 1e4); + } + + function test_shouldCreditGasFees_WhenALotBigger() public { + creditFuzzHelper(30, 1e24); + } + + function creditFuzzHelper(uint8 expectedDigits, uint256 multiplier) public { + uint256 originalAmount = 1000; + uint256 amount = originalAmount * multiplier; + console.log("amount", amount); + + address secondAddress = actor("secondAddress"); + address thirdAddress = actor("thirdAddress"); + + feeCurrencyAdapterForFuzzyTests.initialize( + address(feeCurrency), + "adapter", + "ad", + expectedDigits + ); + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.debitGasFees(address(this), amount); + + vm.prank(address(0)); + feeCurrencyAdapterForFuzzyTests.creditGasFees( + address(this), + secondAddress, + address(0), + thirdAddress, + amount / 4, + amount / 4, + 0, + amount / 4 + ); + assertEq( + feeCurrency.balanceOf(address(this)), + (initialSupply - originalAmount) + (originalAmount / 4) + ); + assertEq(feeCurrency.balanceOf(secondAddress), originalAmount / 4); + assertEq(feeCurrency.balanceOf(thirdAddress), originalAmount / 2); + + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(address(this)), + (initialSupply - originalAmount) * multiplier + ((originalAmount * multiplier) / 4) + ); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(secondAddress), + (originalAmount * multiplier) / 4 + ); + assertEq( + feeCurrencyAdapterForFuzzyTests.balanceOf(thirdAddress), + (originalAmount * multiplier) / 2 + ); + } +} + +contract FeeCurrencyAdapter_UpscaleAndDownScaleTests is FeeCurrencyAdapterTest { + function test_shouldUpscale() public { + assertEq(feeCurrencyAdapter.upscaleVisible(1), 1e12); + assertEq(feeCurrencyAdapter.upscaleVisible(1e6), 1e18); + assertEq(feeCurrencyAdapter.upscaleVisible(1e12), 1e24); + } + + function test_ShouldRevertUpscale_WhenOverflow() public { + uint256 digitDifference = 10**12; + uint256 maxValue = type(uint256).max; + uint256 boundaryValue = maxValue / digitDifference + 1; + + vm.expectRevert(); + feeCurrencyAdapter.upscaleVisible(boundaryValue); + } + + function test_shouldDownscale() public { + assertEq(feeCurrencyAdapter.downscaleVisible(1e12), 1); + assertEq(feeCurrencyAdapter.downscaleVisible(1e18), 1e6); + assertEq(feeCurrencyAdapter.downscaleVisible(1e24), 1e12); + } + + function test_ShouldReturn0_WhenSmallEnough() public { + assertEq(feeCurrencyAdapter.downscaleVisible(1), 0); + assertEq(feeCurrencyAdapter.downscaleVisible(1e6 - 1), 0); + assertEq(feeCurrencyAdapter.downscaleVisible(1e12 - 1), 0); + } +} + +contract FeeCurrencyAdapter_SetAdaptedToken is FeeCurrencyAdapterTest { + function test_shouldRevert_WhenNotCalledByOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + feeCurrencyAdapter.setAdaptedToken(address(0)); + } + + function test_shouldSetAdaptedToken() public { + address newWrappedToken = actor("newWrappedToken"); + feeCurrencyAdapter.setAdaptedToken(newWrappedToken); + assertEq(address(feeCurrencyAdapter.adaptedToken()), newWrappedToken); + assertEq(feeCurrencyAdapter.getAdaptedToken(), newWrappedToken); + } +}