diff --git a/src/circles/Demurrage.sol b/src/circles/Demurrage.sol index 3192ded..ae6526e 100644 --- a/src/circles/Demurrage.sol +++ b/src/circles/Demurrage.sol @@ -261,17 +261,4 @@ contract Demurrage is ICirclesCompactErrors, ICirclesDemurrageErrors { // and do not cache it return Math64x64.pow(GAMMA_64x64, _dayDifference); } - - /** - * Calculate the inflationary balance of a demurraged balance - * @param _balance Demurraged balance to calculate the inflationary balance of - * @param _dayUpdated The day the balance was last updated - */ - function _calculateInflationaryBalance(uint256 _balance, uint256 _dayUpdated) internal pure returns (uint256) { - // calculate the inflationary balance by dividing the balance by GAMMA^days - // note: GAMMA < 1, so dividing by a power of it, returns a bigger number, - // so the numerical imprecision is in the least significant bits. - int128 i = Math64x64.pow(BETA_64x64, _dayUpdated); - return Math64x64.mulu(i, _balance); - } } diff --git a/src/circles/DiscountedBalances.sol b/src/circles/DiscountedBalances.sol index 75ce835..e698608 100644 --- a/src/circles/DiscountedBalances.sol +++ b/src/circles/DiscountedBalances.sol @@ -78,16 +78,6 @@ contract DiscountedBalances is Demurrage { // Internal functions - /** - * @dev Calculate the inflationary balance of a discounted balance - * @param _account Address of the account to calculate the balance of - * @param _id Circles identifier for which to calculate the balance - */ - function _inflationaryBalanceOf(address _account, uint256 _id) internal view returns (uint256) { - DiscountedBalance memory discountedBalance = discountedBalances[_id][_account]; - return _calculateInflationaryBalance(discountedBalance.balance, discountedBalance.lastUpdatedDay); - } - /** * @dev Update the balance of an account for a given Circles identifier * @param _account Address of the account to update the balance of diff --git a/src/circles/InflationaryOperator.sol b/src/circles/InflationaryOperator.sol index 20f542b..36b8ded 100644 --- a/src/circles/InflationaryOperator.sol +++ b/src/circles/InflationaryOperator.sol @@ -94,6 +94,6 @@ contract InflationaryCirclesOperator is BatchedDemurrage { function _inflationaryBalanceOf(address _account, uint256 _id) internal view returns (uint256) { // retrieve the balance in demurrage units (of today) uint256 balance = hub.balanceOf(_account, _id); - return _calculateInflationaryBalance(balance, day(block.timestamp)); + return convertDemurrageToInflationaryValue(balance, day(block.timestamp)); } } diff --git a/src/lift/DemurrageCircles.sol b/src/lift/DemurrageCircles.sol index 5b674b3..1943a54 100644 --- a/src/lift/DemurrageCircles.sol +++ b/src/lift/DemurrageCircles.sol @@ -74,7 +74,7 @@ contract DemurrageCircles is MasterCopyNonUpgradable, ERC20DiscountedBalances, E _burn(msg.sender, _amount); hub.safeTransferFrom(address(this), msg.sender, toTokenId(avatar), _amount, ""); - uint256 inflationaryAmount = _calculateInflationaryBalance(_amount, day(block.timestamp)); + uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_amount, day(block.timestamp)); emit WithdrawDemurraged(msg.sender, _amount, inflationaryAmount); } @@ -107,7 +107,7 @@ contract DemurrageCircles is MasterCopyNonUpgradable, ERC20DiscountedBalances, E if (_id != toTokenId(avatar)) revert CirclesInvalidCirclesId(_id, 0); _mint(_from, _amount); - uint256 inflationaryAmount = _calculateInflationaryBalance(_amount, day(block.timestamp)); + uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_amount, day(block.timestamp)); emit DepositDemurraged(_from, _amount, inflationaryAmount); diff --git a/src/lift/ERC20DiscountedBalances.sol b/src/lift/ERC20DiscountedBalances.sol index 5b206e2..4cfffb5 100644 --- a/src/lift/ERC20DiscountedBalances.sol +++ b/src/lift/ERC20DiscountedBalances.sol @@ -102,11 +102,6 @@ contract ERC20DiscountedBalances is ERC20Permit, BatchedDemurrage, IERC20 { // Internal functions - function _inflationaryBalanceOf(address _account) internal view returns (uint256) { - DiscountedBalance memory discountedBalance = discountedBalances[_account]; - return _calculateInflationaryBalance(discountedBalance.balance, discountedBalance.lastUpdatedDay); - } - function _updateBalance(address _account, uint256 _balance, uint64 _day) internal { if (_balance > MAX_VALUE) { // Balance exceeds maximum value. diff --git a/src/lift/ERC20InflationaryBalances.sol b/src/lift/ERC20InflationaryBalances.sol index bb04e92..9cff8fc 100644 --- a/src/lift/ERC20InflationaryBalances.sol +++ b/src/lift/ERC20InflationaryBalances.sol @@ -6,15 +6,11 @@ import "../circles/BatchedDemurrage.sol"; import "./ERC20Permit.sol"; contract ERC20InflationaryBalances is ERC20Permit, BatchedDemurrage, IERC20 { - // Constants - - uint8 internal constant EXTENDED_ACCURACY_BITS = 64; - // State variables - uint256 internal _extendedTotalSupply; + uint256 internal _totalSupply; - mapping(address => uint256) private _extendedAccuracyBalances; + mapping(address => uint256) private _balances; // Constructor @@ -55,7 +51,7 @@ contract ERC20InflationaryBalances is ERC20Permit, BatchedDemurrage, IERC20 { } function balanceOf(address _account) external view returns (uint256) { - return _extendedAccuracyBalances[_account] >> EXTENDED_ACCURACY_BITS; + return _balances[_account]; } function allowance(address _owner, address _spender) external view returns (uint256) { @@ -63,58 +59,46 @@ contract ERC20InflationaryBalances is ERC20Permit, BatchedDemurrage, IERC20 { } function totalSupply() external view returns (uint256) { - return _extendedTotalSupply >> EXTENDED_ACCURACY_BITS; + return _totalSupply; } // Internal functions - function _convertToExtended(uint256 _amount) internal pure returns (uint256) { - if (_amount > MAX_VALUE) revert CirclesAmountOverflow(_amount, 0); - return _amount << EXTENDED_ACCURACY_BITS; - } - function _transfer(address _from, address _to, uint256 _amount) internal { - uint256 extendedAmount = _convertToExtended(_amount); - uint256 extendedFromBalance = _extendedAccuracyBalances[_from]; - if (extendedFromBalance < extendedAmount) { - revert ERC20InsufficientBalance(_from, extendedFromBalance >> EXTENDED_ACCURACY_BITS, _amount); + uint256 fromBalance = _balances[_from]; + if (fromBalance < _amount) { + revert ERC20InsufficientBalance(_from, fromBalance, _amount); } unchecked { - _extendedAccuracyBalances[_from] = extendedFromBalance - extendedAmount; + _balances[_from] = fromBalance - _amount; // rely on total supply not having overflowed - _extendedAccuracyBalances[_to] += extendedAmount; + _balances[_to] += _amount; } emit Transfer(_from, _to, _amount); } function _mintFromDemurragedAmount(address _owner, uint256 _demurragedAmount) internal returns (uint256) { - // first convert to extended accuracy representation so we have extra garbage bits, - // before we apply the inflation factor, which will produce errors in the least significant bits - uint256 extendedAmount = - _calculateInflationaryBalance(_convertToExtended(_demurragedAmount), day(block.timestamp)); + uint256 inflationaryAmount = convertDemurrageToInflationaryValue(_demurragedAmount, day(block.timestamp)); // here ensure total supply does not overflow - _extendedTotalSupply += extendedAmount; + _totalSupply += inflationaryAmount; unchecked { - _extendedAccuracyBalances[_owner] += extendedAmount; + _balances[_owner] += inflationaryAmount; } - emit Transfer(address(0), _owner, extendedAmount >> EXTENDED_ACCURACY_BITS); + emit Transfer(address(0), _owner, inflationaryAmount); - return extendedAmount >> EXTENDED_ACCURACY_BITS; + return inflationaryAmount; } - function _burn(address _owner, uint256 _amount) internal returns (uint256) { - uint256 extendedAmount = _convertToExtended(_amount); - uint256 extendedOwnerBalance = _extendedAccuracyBalances[_owner]; - if (extendedOwnerBalance < extendedAmount) { - revert ERC20InsufficientBalance(_owner, _extendedAccuracyBalances[_owner], _amount); + function _burn(address _owner, uint256 _amount) internal { + uint256 ownerBalance = _balances[_owner]; + if (ownerBalance < _amount) { + revert ERC20InsufficientBalance(_owner, ownerBalance, _amount); } unchecked { - _extendedAccuracyBalances[_owner] = extendedOwnerBalance - extendedAmount; + _balances[_owner] = ownerBalance - _amount; // rely on total supply tracking complete sum of balances - _extendedTotalSupply -= extendedAmount; + _totalSupply -= _amount; } emit Transfer(_owner, address(0), _amount); - - return extendedAmount; } } diff --git a/src/lift/InflationaryCircles.sol b/src/lift/InflationaryCircles.sol index b488e71..bdd4d83 100644 --- a/src/lift/InflationaryCircles.sol +++ b/src/lift/InflationaryCircles.sol @@ -78,11 +78,10 @@ contract InflationaryCircles is MasterCopyNonUpgradable, ERC20InflationaryBalanc // External functions function unwrap(uint256 _amount) external { - uint256 extendedAmount = _burn(msg.sender, _amount); + _burn(msg.sender, _amount); // calculate demurraged amount in extended accuracy representation // then discard garbage bits by shifting right - uint256 demurragedAmount = - convertInflationaryToDemurrageValue(extendedAmount, day(block.timestamp)) >> EXTENDED_ACCURACY_BITS; + uint256 demurragedAmount = convertInflationaryToDemurrageValue(_amount, day(block.timestamp)); hub.safeTransferFrom(address(this), msg.sender, toTokenId(avatar), demurragedAmount, ""); diff --git a/test/circles/Demurrage.t.sol b/test/circles/Demurrage.t.sol index 0586f75..15f0303 100644 --- a/test/circles/Demurrage.t.sol +++ b/test/circles/Demurrage.t.sol @@ -42,6 +42,8 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation { startTime(); demurrage = new MockDemurrage(); + + demurrage.setInflationDayZero(INFLATION_DAY_ZERO); } // Tests @@ -55,4 +57,124 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation { ); } } + + // Test the inversion accuracy of the gamma and beta exponentiation over 20 and 100 years + // with and without the extension + // conclusion: we can just drop the extension as the 64x64 fixed point is accurate, and unsure if the extension + // is actually doing something? -- maybe not because GAMMA and BETA have a fixed precision, so they will always + // introduce errors at their precision level... so the extension is pointless and costs extra gas + + // (note) leaving these tests here for now, to document why the extension is removed from Inflationary ERC20 in patch21! + // this can later be tidied up and removed + + function testInversionGammaBeta64x64_20years() public { + // for the coming 20 years (2024 is year 4 since INFLATION_DAY_ZERO) + // check that simply exponentiating the number of days remains accurate + // and without overflow + + // one year in unix time (approximately) + uint256 oneYear = 365 * 24 * 3600; + + for (uint256 i = 0; i <= 20; i++) { + uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear; + uint64 dayCount = demurrage.day(secondsNow); + // convert one CRC to inflationary value + uint256 inflationaryOneCRC = demurrage.convertDemurrageToInflationaryValue(100 * CRC, dayCount); + // now invert the operation + uint256 demurrageOneCRC = demurrage.convertInflationaryToDemurrageValue(inflationaryOneCRC, dayCount); + assertTrue(relativeApproximatelyEqual(100 * CRC, demurrageOneCRC, 1000 * DUST)); + console.log("year ", i, ": ", demurrageOneCRC); + } + } + + function testInversionGammaBeta64x64_100years() public { + // for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO) + // check that simply exponentiating the number of days remains accurate + // and without overflow + + // one year in unix time (approximately) + uint256 oneYear = 365 * 24 * 3600; + + for (uint256 i = 0; i <= 100; i++) { + uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear; + uint64 dayCount = demurrage.day(secondsNow); + // convert one CRC to inflationary value + uint256 inflationaryOneCRC = demurrage.convertDemurrageToInflationaryValue(CRC, dayCount); + // now invert the operation + uint256 demurrageOneCRC = demurrage.convertInflationaryToDemurrageValue(inflationaryOneCRC, dayCount); + assertTrue(relativeApproximatelyEqual(CRC, demurrageOneCRC, 1000 * DUST)); + } + } + + function testInversionGammaBeta64x64_100years_withExtension() public { + // for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO) + // check that simply exponentiating the number of days remains accurate + // and without overflow + + // one year in unix time (approximately) + uint256 oneYear = 365 * 24 * 3600; + + uint8 accuracy_shift = 64; + + uint192 amount = uint192(10000000 * CRC); + console.log("amount: ", amount); + + for (uint256 i = 0; i <= 100; i++) { + uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear; + uint64 dayCount = demurrage.day(secondsNow); + // convert one CRC to inflationary value + uint256 extendedAmount = amount << accuracy_shift; + uint256 inflationaryAmountExtended = demurrage.convertDemurrageToInflationaryValue(extendedAmount, dayCount); + uint256 trimmedInflationaryAmount = inflationaryAmountExtended >> accuracy_shift; + // now invert the operation + uint256 extendedInflationAmountTrimmed = trimmedInflationaryAmount << accuracy_shift; + uint256 demurrageAmountExtended = + demurrage.convertInflationaryToDemurrageValue(extendedInflationAmountTrimmed, dayCount); + uint256 trimmedDemurrageAmount = demurrageAmountExtended >> accuracy_shift; + assertTrue(relativeApproximatelyEqual(amount, trimmedDemurrageAmount, 1000 * DUST)); + console.log("year ", i, ": ", trimmedDemurrageAmount); + } + } + + function testInversionGammaBeta64x64_100years_withExtension_comparison() public { + // for the coming 100 years (2024 is year 4 since INFLATION_DAY_ZERO) + // check that simply exponentiating the number of days remains accurate + // and without overflow + + // one year in unix time (approximately) + uint256 oneYear = 365 * 24 * 3600; + + uint8 accuracy_shift = 64; + + uint192 amount = uint192(254516523121 * CRC); + console.log("amount: ", amount); + + for (uint256 i = 0; i <= 100; i++) { + uint256 secondsNow = INFLATION_DAY_ZERO + i * oneYear; + uint64 dayCount = demurrage.day(secondsNow); + // convert one CRC to inflationary value + uint256 extendedAmount = amount << accuracy_shift; + uint256 inflationaryAmountExtended = demurrage.convertDemurrageToInflationaryValue(extendedAmount, dayCount); + uint256 trimmedInflationaryAmount = inflationaryAmountExtended >> accuracy_shift; + // now invert the operation + uint256 extendedInflationAmountTrimmed = trimmedInflationaryAmount << accuracy_shift; + uint256 demurrageAmountExtended = + demurrage.convertInflationaryToDemurrageValue(extendedInflationAmountTrimmed, dayCount); + uint256 trimmedDemurrageAmount = demurrageAmountExtended >> accuracy_shift; + + // now do the same without extension + uint256 inflationaryAmount_withoutExtension = + demurrage.convertDemurrageToInflationaryValue(amount, dayCount); + // now invert the operation + uint256 demurrageAmount_withoutExtension = + demurrage.convertInflationaryToDemurrageValue(inflationaryAmount_withoutExtension, dayCount); + + uint256 diff = demurrageAmount_withoutExtension > trimmedDemurrageAmount + ? demurrageAmount_withoutExtension - trimmedDemurrageAmount + : trimmedDemurrageAmount - demurrageAmount_withoutExtension; + + assertTrue(diff == 0); + console.log(trimmedDemurrageAmount, " vs ", demurrageAmount_withoutExtension); + } + } } diff --git a/test/circles/MockDemurrage.sol b/test/circles/MockDemurrage.sol index 04faeb2..130f65e 100644 --- a/test/circles/MockDemurrage.sol +++ b/test/circles/MockDemurrage.sol @@ -14,6 +14,10 @@ contract MockDemurrage is Demurrage { return GAMMA_64x64; } + function beta_64x64() external pure returns (int128) { + return BETA_64x64; + } + function r(uint256 _i) external view returns (int128) { return R[_i]; } diff --git a/test/lift/ERC20Demurrage.sol b/test/lift/ERC20Demurrage.t.sol similarity index 100% rename from test/lift/ERC20Demurrage.sol rename to test/lift/ERC20Demurrage.t.sol diff --git a/test/lift/ERC20Inflationary.t.sol b/test/lift/ERC20Inflationary.t.sol new file mode 100644 index 0000000..7705ee2 --- /dev/null +++ b/test/lift/ERC20Inflationary.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import "forge-std/console.sol"; +import "../../src/circles/Demurrage.sol"; +import "../setup/TimeCirclesSetup.sol"; +import "../setup/HumanRegistration.sol"; +import "../hub/MockDeployment.sol"; +import "../hub/MockHub.sol"; +import "../utils/Approximation.sol"; + +contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration, Approximation { + // State variables + + MockDeployment public mockDeployment; + MockHub public hub; + + mapping(address => InflationaryCircles) public erc20s; + + // Constructor + + constructor() HumanRegistration(2) {} + + // Setup + + function setUp() public { + // Set time in 2021 + startTime(); + + // Mock deployment + mockDeployment = new MockDeployment(INFLATION_DAY_ZERO, 365 days); + hub = mockDeployment.hub(); + + // register Alice and Bob + for (uint256 i = 0; i < 2; i++) { + vm.startPrank(addresses[i]); + hub.registerHumanUnrestricted(); + mockDeployment.nameRegistry().registerShortName(); + vm.stopPrank(); + } + + // skip time and mint + skipTime(14 days); + for (uint256 i = 0; i < 2; i++) { + vm.prank(addresses[i]); + hub.personalMintWithoutV1Check(); + } + + // lift some CRC into respective inflationary ERC20's + for (uint256 i = 0; i < 2; i++) { + vm.prank(addresses[i]); + hub.wrap(addresses[i], 100 * CRC, CirclesType.Inflation); + } + + // get ERC20's + for (uint256 i = 0; i < 2; i++) { + erc20s[addresses[i]] = + InflationaryCircles(mockDeployment.erc20Lift().erc20Circles(CirclesType.Inflation, addresses[i])); + } + } + + // Tests + + function testWrapAndUnwrapInflationaryERC20() public { + InflationaryCircles aliceERC20 = erc20s[addresses[0]]; + uint256 aliceId = hub.toTokenId(addresses[0]); + + // Alice first clears her balance on the hub + uint256 aliceBalance = hub.balanceOf(addresses[0], aliceId); + console.log("Alice balance on hub: ", aliceBalance); + vm.prank(addresses[0]); + hub.burn(aliceId, aliceBalance, ""); + aliceBalance = hub.balanceOf(addresses[0], aliceId); + console.log("Alice balance on hub after burn: ", aliceBalance); + + // Alice unwraps her 100 CRC (demurrage) + // first get her balance in inflationary ERC20 + uint256 aliceERC20Balance = aliceERC20.balanceOf(addresses[0]); + console.log("Alice balance in inflationary ERC20: ", aliceERC20Balance); + vm.prank(addresses[0]); + aliceERC20.unwrap(aliceERC20Balance); + aliceERC20Balance = aliceERC20.balanceOf(addresses[0]); + console.log("Alice balance in inflationary ERC20 after unwrap: ", aliceERC20Balance); + + // Alice balance on hub should be 100 CRC again + aliceBalance = hub.balanceOf(addresses[0], aliceId); + console.log("Alice balance on hub after unwrap: ", aliceBalance); + assertTrue(relativeApproximatelyEqual(aliceBalance, 100 * CRC, 10 * DUST)); + } +} diff --git a/test/setup/TimeCirclesSetup.sol b/test/setup/TimeCirclesSetup.sol index e496974..350ed3f 100644 --- a/test/setup/TimeCirclesSetup.sol +++ b/test/setup/TimeCirclesSetup.sol @@ -7,7 +7,7 @@ import {StdCheats} from "forge-std/StdCheats.sol"; contract TimeCirclesSetup is Test { // Constants - uint256 internal constant CRC = uint256(10 ** 18); + uint192 internal constant CRC = uint192(10 ** 18); /** * Arbitrary origin for counting time since 10 December 2021