From 20d7e1798082db95a8633c0eed5e31266a9bf35e Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 3 Apr 2024 18:55:39 +0100 Subject: [PATCH 1/8] (test): simplify mock test framework for deployment --- src/lift/ERC20Lift.sol | 7 +- src/treasury/standardTreasury.sol | 2 +- test/hub/MockDeployment.sol | 118 ++++++++++++++++++++++++++++++ test/hub/V1MintStatusUpdate.t.sol | 27 ++++--- test/lift/ERC20Lift.t.sol | 26 +++++++ test/lift/MockERC20Lift.sol | 19 +++++ test/names/MockNameRegistry.sol | 17 ----- 7 files changed, 181 insertions(+), 35 deletions(-) create mode 100644 test/hub/MockDeployment.sol create mode 100644 test/lift/ERC20Lift.t.sol create mode 100644 test/lift/MockERC20Lift.sol delete mode 100644 test/names/MockNameRegistry.sol diff --git a/src/lift/ERC20Lift.sol b/src/lift/ERC20Lift.sol index ad16cd8..a3fb8b4 100644 --- a/src/lift/ERC20Lift.sol +++ b/src/lift/ERC20Lift.sol @@ -15,9 +15,9 @@ contract ERC20Lift is ProxyFactory, IERC20Lift, ICirclesErrors { // State variables - IHubV2 public immutable hub; + IHubV2 public hub; - INameRegistry public immutable nameRegistry; + INameRegistry public nameRegistry; /** * @dev The master copy of the ERC20 demurrage and inflation Circles contract. @@ -94,7 +94,8 @@ contract ERC20Lift is ProxyFactory, IERC20Lift, ICirclesErrors { // Internal functions function _deployERC20(address _masterCopy, address _avatar) internal returns (address) { - bytes memory wrapperSetupData = abi.encodeWithSelector(ERC20_WRAPPER_SETUP_CALLPREFIX, hub, _avatar); + bytes memory wrapperSetupData = + abi.encodeWithSelector(ERC20_WRAPPER_SETUP_CALLPREFIX, hub, nameRegistry, _avatar); address erc20wrapper = address(_createProxy(_masterCopy, wrapperSetupData)); return erc20wrapper; } diff --git a/src/treasury/standardTreasury.sol b/src/treasury/standardTreasury.sol index a50cffd..a1ec22e 100644 --- a/src/treasury/standardTreasury.sol +++ b/src/treasury/standardTreasury.sol @@ -31,7 +31,7 @@ contract StandardTreasury is /** * @notice Address of the hub contract */ - IHubV2 public immutable hub; + IHubV2 public hub; /** * @notice Address of the mastercopy standard vault contract diff --git a/test/hub/MockDeployment.sol b/test/hub/MockDeployment.sol new file mode 100644 index 0000000..5de8cc6 --- /dev/null +++ b/test/hub/MockDeployment.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/hub/Hub.sol"; +import "../../src/lift/IERC20Lift.sol"; +import "../../src/migration/IHub.sol"; +import "../../src/names/INameRegistry.sol"; +import "../../src/names/NameRegistry.sol"; +import "../../src/treasury/standardVault.sol"; +import "../../src/treasury/StandardTreasury.sol"; +import "../../src/lift/DemurrageCircles.sol"; +import "../../src/lift/InflationaryCircles.sol"; +import "../lift/MockERC20Lift.sol"; + +contract MockHub is Hub { + // Constructor + + constructor(uint256 _inflationDayZero, uint256 _bootstrapTime) + Hub( + IHubV1(address(1)), + INameRegistry(address(1)), + address(1), + IERC20Lift(address(1)), + address(1), + _inflationDayZero, + _bootstrapTime, + "" + ) + {} + + // External functions + + function setSiblings(address _migration, address _nameRegistry, address _liftERC20, address _standardTreasury) + external + { + migration = _migration; + nameRegistry = INameRegistry(_nameRegistry); + liftERC20 = ERC20Lift(_liftERC20); + standardTreasury = _standardTreasury; + } + + function registerHumanUnrestricted() external { + address human = msg.sender; + + // insert avatar into linked list; reverts if it already exists + _insertAvatar(human); + + require(avatars[human] != address(0), "MockPathTransferHub: avatar not found"); + + // set the last mint time to the current timestamp for invited human + // and register the v1 Circles contract status as unregistered + address v1CirclesStatus = address(0); + MintTime storage mintTime = mintTimes[human]; + mintTime.mintV1Status = v1CirclesStatus; + mintTime.lastMintTime = uint96(block.timestamp); + + // trust self indefinitely, cannot be altered later + _trust(human, human, INDEFINITE_FUTURE); + } + + function personalMintWithoutV1Check() external { + require(isHuman(msg.sender), "MockPathTransferHub: not a human"); + require(avatars[msg.sender] != address(0), "MockPathTransferHub: avatar not found"); + address human = msg.sender; + + // skips checks in v1 mint for tests + + // mint Circles for the human + _claimIssuance(human); + } + + // Public functions + + function accessUnpackCoordinates(bytes calldata _packedData, uint256 _numberOfTriplets) + public + pure + returns (uint16[] memory unpackedCoordinates_) + { + return super._unpackCoordinates(_packedData, _numberOfTriplets); + } + + // Private functions + + function notMocked() private pure { + assert(false); + } +} + +contract MockDeployment { + // State variables + + MockHub public hub; + NameRegistry public nameRegistry; + StandardTreasury public treasury; + StandardVault public masterCopyVault; + MockERC20Lift public erc20Lift; + DemurrageCircles public mastercopyDemurrageCircles; + InflationaryCircles public mastercopyInflationaryCircles; + + constructor(uint256 _inflationDayZero, uint256 _bootstrapTime) { + // deploy mastercopies + masterCopyVault = new StandardVault(); + mastercopyDemurrageCircles = new DemurrageCircles(); + mastercopyInflationaryCircles = new InflationaryCircles(); + + // deploy mocks + hub = new MockHub(_inflationDayZero, _bootstrapTime); + erc20Lift = new MockERC20Lift(address(mastercopyDemurrageCircles), address(mastercopyInflationaryCircles)); + + // the following only depend on knowing hub, so we can deploy with mocking + nameRegistry = new NameRegistry(IHubV2(address(hub))); + treasury = new StandardTreasury(IHubV2(address(hub)), address(masterCopyVault)); + + // we don't care to set migration so leave that as 0x01 + hub.setSiblings(address(1), address(nameRegistry), address(erc20Lift), address(treasury)); + erc20Lift.setSiblings(IHubV2(address(hub)), INameRegistry(address(nameRegistry))); + } +} diff --git a/test/hub/V1MintStatusUpdate.t.sol b/test/hub/V1MintStatusUpdate.t.sol index f5eab7d..f79f2ba 100644 --- a/test/hub/V1MintStatusUpdate.t.sol +++ b/test/hub/V1MintStatusUpdate.t.sol @@ -6,25 +6,21 @@ import {StdCheats} from "forge-std/StdCheats.sol"; import "forge-std/console.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../src/migration/IToken.sol"; -import "../migration/MockMigration.sol"; -import "../names/MockNameRegistry.sol"; +import "../../src/migration/Migration.sol"; +import "../../src/names/NameRegistry.sol"; import "../setup/TimeCirclesSetup.sol"; import "../setup/HumanRegistration.sol"; import "../migration/MockHub.sol"; import "./MockMigrationHub.sol"; contract V1MintStatusUpdateTest is Test, TimeCirclesSetup, HumanRegistration { - // Constants - - bytes32 private constant SALT = keccak256("CirclesV2:V1MintStatusUpdateTest"); - // State variables MockMigrationHub public mockHub; MockHubV1 public mockHubV1; - MockNameRegistry public nameRegistry; - MockMigration public migration; + NameRegistry public nameRegistry; + Migration public migration; // Constructor @@ -36,14 +32,17 @@ contract V1MintStatusUpdateTest is Test, TimeCirclesSetup, HumanRegistration { // Set time in 2021 startTime(); + // Mock hub v1 mockHubV1 = new MockHubV1(); - // First deploy the contracts to know the addresses - migration = new MockMigration(mockHubV1, IHubV2(address(1))); - nameRegistry = new MockNameRegistry(IHubV2(address(1))); + + // First deploy the mock hub v2 so that we have the address mockHub = new MockMigrationHub(mockHubV1, address(2), INFLATION_DAY_ZERO, 365 days); - // then set the addresses in the respective contracts - migration.setHubV2(IHubV2(address(mockHub))); - nameRegistry.setHubV2(IHubV2(address(mockHub))); + + // Name registry and migration do not need to be mocked + nameRegistry = new NameRegistry(IHubV2(address(mockHub))); + migration = new Migration(mockHubV1, IHubV2(address(mockHub))); + + // update hub v2 with the new addresses of the name registry and migration mockHub.setSiblings(address(migration), address(nameRegistry)); } diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol new file mode 100644 index 0000000..d25adad --- /dev/null +++ b/test/lift/ERC20Lift.t.sol @@ -0,0 +1,26 @@ +// 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 "../setup/TimeCirclesSetup.sol"; +import "../setup/HumanRegistration.sol"; +import "../hub/MockPathTransferHub.sol"; + +contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { + // State variables + + MockPathTransferHub public mockHub; + + // Constructor + + constructor() HumanRegistration(2) {} + + // Setup + + function setUp() public { + // Set time in 2021 + startTime(); + } +} diff --git a/test/lift/MockERC20Lift.sol b/test/lift/MockERC20Lift.sol new file mode 100644 index 0000000..aab2465 --- /dev/null +++ b/test/lift/MockERC20Lift.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/lift/ERC20Lift.sol"; + +contract MockERC20Lift is ERC20Lift { + // Constructor + + constructor(address _mastercopyERC20Demurrage, address _mastercopyERC20Inflation) + ERC20Lift(IHubV2(address(1)), INameRegistry(address(1)), _mastercopyERC20Demurrage, _mastercopyERC20Inflation) + {} + + // External functions + + function setSiblings(IHubV2 _hub, INameRegistry _nameRegistry) external { + hub = _hub; + nameRegistry = _nameRegistry; + } +} diff --git a/test/names/MockNameRegistry.sol b/test/names/MockNameRegistry.sol deleted file mode 100644 index fc074f0..0000000 --- a/test/names/MockNameRegistry.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.13; - -import "../../src/hub/Hub.sol"; -import "../../src/names/NameRegistry.sol"; - -contract MockNameRegistry is NameRegistry { - // Constructor - - constructor(IHubV2 _hubV2) NameRegistry(_hubV2) {} - - // External functions - - function setHubV2(IHubV2 _hubV2) external { - hub = _hubV2; - } -} From 0857a602b0de3d2eebe8c5a92cbd46ac0c3d537b Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 3 Apr 2024 19:16:50 +0100 Subject: [PATCH 2/8] (hub, nameregistry): fix mismatch on interface --- src/hub/Hub.sol | 10 ++--- src/names/INameRegistry.sol | 4 +- src/names/NameRegistry.sol | 3 +- test/hub/MockDeployment.sol | 77 +---------------------------------- test/hub/MockHub.sol | 81 +++++++++++++++++++++++++++++++++++++ test/lift/ERC20Lift.t.sol | 10 ++++- 6 files changed, 100 insertions(+), 85 deletions(-) create mode 100644 test/hub/MockHub.sol diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index a9e0dbb..6bb97ff 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -277,8 +277,8 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { _registerGroup(msg.sender, _mint, standardTreasury, _name, _symbol); // for groups register possible custom name and symbol - nameRegistry.registerName(msg.sender, _name); - nameRegistry.registerSymbol(msg.sender, _symbol); + nameRegistry.registerCustomName(msg.sender, _name); + nameRegistry.registerCustomSymbol(msg.sender, _symbol); // store the IPFS CIDv0 digest for the group metadata nameRegistry.updateCidV0Digest(msg.sender, _cidV0Digest); @@ -304,8 +304,8 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { _registerGroup(msg.sender, _mint, _treasury, _name, _symbol); // for groups register possible custom name and symbol - nameRegistry.registerName(msg.sender, _name); - nameRegistry.registerSymbol(msg.sender, _symbol); + nameRegistry.registerCustomName(msg.sender, _name); + nameRegistry.registerCustomSymbol(msg.sender, _symbol); // store the IPFS CIDv0 digest for the group metadata nameRegistry.updateCidV0Digest(msg.sender, _cidV0Digest); @@ -322,7 +322,7 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { _insertAvatar(msg.sender); // for organizations, only register possible custom name - nameRegistry.registerName(msg.sender, _name); + nameRegistry.registerCustomName(msg.sender, _name); // store the IPFS CIDv0 digest for the organization metadata nameRegistry.updateCidV0Digest(msg.sender, _cidV0Digest); diff --git a/src/names/INameRegistry.sol b/src/names/INameRegistry.sol index b77fc29..da2cef1 100644 --- a/src/names/INameRegistry.sol +++ b/src/names/INameRegistry.sol @@ -3,8 +3,8 @@ pragma solidity >=0.8.13; interface INameRegistry { function updateCidV0Digest(address avatar, bytes32 cidVoDigest) external; - function registerName(address avatar, string calldata name) external; - function registerSymbol(address avatar, string calldata symbol) external; + function registerCustomName(address avatar, string calldata name) external; + function registerCustomSymbol(address avatar, string calldata symbol) external; function name(address avatar) external view returns (string memory); function symbol(address avatar) external view returns (string memory); diff --git a/src/names/NameRegistry.sol b/src/names/NameRegistry.sol index cb159dc..3e4ce78 100644 --- a/src/names/NameRegistry.sol +++ b/src/names/NameRegistry.sol @@ -4,8 +4,9 @@ pragma solidity >=0.8.13; import "../errors/Errors.sol"; import "../hub/IHub.sol"; import "./Base58Converter.sol"; +import "./INameRegistry.sol"; -contract NameRegistry is Base58Converter, INameRegistryErrors, ICirclesErrors { +contract NameRegistry is Base58Converter, INameRegistry, INameRegistryErrors, ICirclesErrors { // Constants /** diff --git a/test/hub/MockDeployment.sol b/test/hub/MockDeployment.sol index 5de8cc6..509322c 100644 --- a/test/hub/MockDeployment.sol +++ b/test/hub/MockDeployment.sol @@ -11,80 +11,7 @@ import "../../src/treasury/StandardTreasury.sol"; import "../../src/lift/DemurrageCircles.sol"; import "../../src/lift/InflationaryCircles.sol"; import "../lift/MockERC20Lift.sol"; - -contract MockHub is Hub { - // Constructor - - constructor(uint256 _inflationDayZero, uint256 _bootstrapTime) - Hub( - IHubV1(address(1)), - INameRegistry(address(1)), - address(1), - IERC20Lift(address(1)), - address(1), - _inflationDayZero, - _bootstrapTime, - "" - ) - {} - - // External functions - - function setSiblings(address _migration, address _nameRegistry, address _liftERC20, address _standardTreasury) - external - { - migration = _migration; - nameRegistry = INameRegistry(_nameRegistry); - liftERC20 = ERC20Lift(_liftERC20); - standardTreasury = _standardTreasury; - } - - function registerHumanUnrestricted() external { - address human = msg.sender; - - // insert avatar into linked list; reverts if it already exists - _insertAvatar(human); - - require(avatars[human] != address(0), "MockPathTransferHub: avatar not found"); - - // set the last mint time to the current timestamp for invited human - // and register the v1 Circles contract status as unregistered - address v1CirclesStatus = address(0); - MintTime storage mintTime = mintTimes[human]; - mintTime.mintV1Status = v1CirclesStatus; - mintTime.lastMintTime = uint96(block.timestamp); - - // trust self indefinitely, cannot be altered later - _trust(human, human, INDEFINITE_FUTURE); - } - - function personalMintWithoutV1Check() external { - require(isHuman(msg.sender), "MockPathTransferHub: not a human"); - require(avatars[msg.sender] != address(0), "MockPathTransferHub: avatar not found"); - address human = msg.sender; - - // skips checks in v1 mint for tests - - // mint Circles for the human - _claimIssuance(human); - } - - // Public functions - - function accessUnpackCoordinates(bytes calldata _packedData, uint256 _numberOfTriplets) - public - pure - returns (uint16[] memory unpackedCoordinates_) - { - return super._unpackCoordinates(_packedData, _numberOfTriplets); - } - - // Private functions - - function notMocked() private pure { - assert(false); - } -} +import "./MockHub.sol"; contract MockDeployment { // State variables @@ -112,7 +39,7 @@ contract MockDeployment { treasury = new StandardTreasury(IHubV2(address(hub)), address(masterCopyVault)); // we don't care to set migration so leave that as 0x01 - hub.setSiblings(address(1), address(nameRegistry), address(erc20Lift), address(treasury)); + hub.setSiblings(address(1), INameRegistry(address(nameRegistry)), erc20Lift, address(treasury)); erc20Lift.setSiblings(IHubV2(address(hub)), INameRegistry(address(nameRegistry))); } } diff --git a/test/hub/MockHub.sol b/test/hub/MockHub.sol new file mode 100644 index 0000000..69ba230 --- /dev/null +++ b/test/hub/MockHub.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/hub/Hub.sol"; + +contract MockHub is Hub { + // Constructor + + constructor(uint256 _inflationDayZero, uint256 _bootstrapTime) + Hub( + IHubV1(address(1)), + INameRegistry(address(1)), + address(1), + IERC20Lift(address(1)), + address(1), + _inflationDayZero, + _bootstrapTime, + "" + ) + {} + + // External functions + + function setSiblings( + address _migration, + INameRegistry _nameRegistry, + IERC20Lift _liftERC20, + address _standardTreasury + ) external { + migration = _migration; + nameRegistry = INameRegistry(_nameRegistry); + liftERC20 = IERC20Lift(_liftERC20); + standardTreasury = _standardTreasury; + } + + function registerHumanUnrestricted() external { + address human = msg.sender; + + // insert avatar into linked list; reverts if it already exists + _insertAvatar(human); + + require(avatars[human] != address(0), "MockPathTransferHub: avatar not found"); + + // set the last mint time to the current timestamp for invited human + // and register the v1 Circles contract status as unregistered + address v1CirclesStatus = address(0); + MintTime storage mintTime = mintTimes[human]; + mintTime.mintV1Status = v1CirclesStatus; + mintTime.lastMintTime = uint96(block.timestamp); + + // trust self indefinitely, cannot be altered later + _trust(human, human, INDEFINITE_FUTURE); + } + + function personalMintWithoutV1Check() external { + require(isHuman(msg.sender), "MockPathTransferHub: not a human"); + require(avatars[msg.sender] != address(0), "MockPathTransferHub: avatar not found"); + address human = msg.sender; + + // skips checks in v1 mint for tests + + // mint Circles for the human + _claimIssuance(human); + } + + // Public functions + + function accessUnpackCoordinates(bytes calldata _packedData, uint256 _numberOfTriplets) + public + pure + returns (uint16[] memory unpackedCoordinates_) + { + return super._unpackCoordinates(_packedData, _numberOfTriplets); + } + + // Private functions + + function notMocked() private pure { + assert(false); + } +} diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol index d25adad..84cf3a8 100644 --- a/test/lift/ERC20Lift.t.sol +++ b/test/lift/ERC20Lift.t.sol @@ -6,12 +6,14 @@ import {StdCheats} from "forge-std/StdCheats.sol"; import "forge-std/console.sol"; import "../setup/TimeCirclesSetup.sol"; import "../setup/HumanRegistration.sol"; -import "../hub/MockPathTransferHub.sol"; +import "../hub/MockDeployment.sol"; +import "../hub/MockHub.sol"; contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { // State variables - MockPathTransferHub public mockHub; + MockDeployment public mockDeployment; + MockHub public mockHub; // Constructor @@ -22,5 +24,9 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { function setUp() public { // Set time in 2021 startTime(); + + // Mock deployment + mockDeployment = new MockDeployment(INFLATION_DAY_ZERO, 365 days); + mockHub = mockDeployment.hub(); } } From 3f4c05777f71d51971a7742643440f077a5a9872 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 3 Apr 2024 20:41:08 +0100 Subject: [PATCH 3/8] (test/lift): resolved first Proxy error; now unsure what reverts --- src/lift/DemurrageCircles.sol | 5 +++-- src/lift/InflationaryCircles.sol | 5 +++-- src/proxy/Proxy.sol | 2 +- test/hub/MockDeployment.sol | 4 ++-- test/lift/ERC20Lift.t.sol | 30 ++++++++++++++++++++++++++++-- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/lift/DemurrageCircles.sol b/src/lift/DemurrageCircles.sol index 6b98aca..cd82e29 100644 --- a/src/lift/DemurrageCircles.sol +++ b/src/lift/DemurrageCircles.sol @@ -42,7 +42,7 @@ contract DemurrageCircles is ERC20DiscountedBalances, ERC1155Holder { // Setup function - function setup(IHubV2 _hub, INameRegistry _nameRegistry, address _avatar) external { + function setup(address _hub, address _nameRegistry, address _avatar) external { if (address(hub) != address(0)) { revert CirclesProxyAlreadyInitialized(); } @@ -56,8 +56,9 @@ contract DemurrageCircles is ERC20DiscountedBalances, ERC1155Holder { if (_avatar == address(0)) { revert CirclesAddressCannotBeZero(2); } - hub = _hub; + hub = IHubV2(_hub); avatar = _avatar; + nameRegistry = INameRegistry(_nameRegistry); // read inflation day zero from hub inflationDayZero = hub.inflationDayZero(); diff --git a/src/lift/InflationaryCircles.sol b/src/lift/InflationaryCircles.sol index 3dcacfb..1eba6e6 100644 --- a/src/lift/InflationaryCircles.sol +++ b/src/lift/InflationaryCircles.sol @@ -42,7 +42,7 @@ contract InflationaryCircles is ERC20InflationaryBalances, ERC1155Holder { // Setup function - function setup(IHubV2 _hub, INameRegistry _nameRegistry, address _avatar) external { + function setup(address _hub, address _nameRegistry, address _avatar) external { if (address(hub) != address(0)) { // Must not be initialized already. revert CirclesProxyAlreadyInitialized(); @@ -59,8 +59,9 @@ contract InflationaryCircles is ERC20InflationaryBalances, ERC1155Holder { // Must not be the zero address. revert CirclesAddressCannotBeZero(2); } - hub = _hub; + hub = IHubV2(_hub); avatar = _avatar; + nameRegistry = INameRegistry(_nameRegistry); // read inflation day zero from hub inflationDayZero = hub.inflationDayZero(); diff --git a/src/proxy/Proxy.sol b/src/proxy/Proxy.sol index 774ea93..56a7a77 100755 --- a/src/proxy/Proxy.sol +++ b/src/proxy/Proxy.sol @@ -24,7 +24,7 @@ contract Proxy is ICirclesErrors { /// @dev Constructor function sets address of master copy contract. /// @param _masterCopy Master copy address. constructor(address _masterCopy) { - if (_masterCopy != address(0)) { + if (_masterCopy == address(0)) { // Invalid master copy address provided revert CirclesAddressCannotBeZero(0); } diff --git a/test/hub/MockDeployment.sol b/test/hub/MockDeployment.sol index 509322c..3088dcb 100644 --- a/test/hub/MockDeployment.sol +++ b/test/hub/MockDeployment.sol @@ -39,7 +39,7 @@ contract MockDeployment { treasury = new StandardTreasury(IHubV2(address(hub)), address(masterCopyVault)); // we don't care to set migration so leave that as 0x01 - hub.setSiblings(address(1), INameRegistry(address(nameRegistry)), erc20Lift, address(treasury)); - erc20Lift.setSiblings(IHubV2(address(hub)), INameRegistry(address(nameRegistry))); + hub.setSiblings(address(1), nameRegistry, erc20Lift, address(treasury)); + erc20Lift.setSiblings(IHubV2(address(hub)), nameRegistry); } } diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol index 84cf3a8..57c8462 100644 --- a/test/lift/ERC20Lift.t.sol +++ b/test/lift/ERC20Lift.t.sol @@ -13,7 +13,7 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { // State variables MockDeployment public mockDeployment; - MockHub public mockHub; + MockHub public hub; // Constructor @@ -27,6 +27,32 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { // Mock deployment mockDeployment = new MockDeployment(INFLATION_DAY_ZERO, 365 days); - mockHub = mockDeployment.hub(); + hub = mockDeployment.hub(); + } + + // Tests + + function testERC20Wrap() public { + // register Alice + vm.prank(addresses[0]); + hub.registerHumanUnrestricted(); + + // skip time and mint + skipTime(14 days); + vm.prank(addresses[0]); + hub.personalMintWithoutV1Check(); + + // test the master contracts in Lift + ERC20Lift lift = mockDeployment.erc20Lift(); + DemurrageCircles demurrage = mockDeployment.mastercopyDemurrageCircles(); + address demurrageMasterCopy = lift.masterCopyERC20Wrapper(uint256(CirclesType.Demurrage)); + assertEq(demurrageMasterCopy, address(demurrage)); + + console.log("hub address: ", address(hub)); + + // wrap some into demurrage ERC20 + console.log("Circles type Demurrage: ", uint256(CirclesType.Demurrage)); + vm.prank(addresses[0]); + hub.wrap(addresses[0], 10 * CRC, CirclesType.Demurrage); } } From 9053fa3e05f09d1a5ffe5e75230f1c4643f2e004 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 3 Apr 2024 20:57:34 +0100 Subject: [PATCH 4/8] (test/proxy): so proxy stores different address under mastercopy --- src/proxy/Proxy.sol | 6 +++--- test/lift/ERC20Lift.t.sol | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/proxy/Proxy.sol b/src/proxy/Proxy.sol index 56a7a77..198db96 100755 --- a/src/proxy/Proxy.sol +++ b/src/proxy/Proxy.sol @@ -13,20 +13,20 @@ interface IProxy { /// applying the code of a master contract. /// @author Stefan George - /// @author Richard Meissner - -contract Proxy is ICirclesErrors { +contract Proxy { // masterCopy always needs to be first declared variable, // to ensure that it is at the same location in the contracts // to which calls are delegated. // To reduce deployment costs this variable is internal // and needs to be retrieved via `getStorageAt` - address internal masterCopy; + address public masterCopy; /// @dev Constructor function sets address of master copy contract. /// @param _masterCopy Master copy address. constructor(address _masterCopy) { if (_masterCopy == address(0)) { // Invalid master copy address provided - revert CirclesAddressCannotBeZero(0); + revert() ; } masterCopy = _masterCopy; } diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol index 57c8462..6214742 100644 --- a/test/lift/ERC20Lift.t.sol +++ b/test/lift/ERC20Lift.t.sol @@ -50,9 +50,12 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { console.log("hub address: ", address(hub)); + DemurrageCircles proxyERC20D = DemurrageCircles(lift.ensureERC20(addresses[0], CirclesType.Demurrage)); + console.log("proxyERC20D address: ", address(proxyERC20D)); + assertEq(Proxy(payable(address(proxyERC20D))).masterCopy(), address(demurrage)); + // wrap some into demurrage ERC20 - console.log("Circles type Demurrage: ", uint256(CirclesType.Demurrage)); - vm.prank(addresses[0]); - hub.wrap(addresses[0], 10 * CRC, CirclesType.Demurrage); + // vm.prank(addresses[0]); + // hub.wrap(addresses[0], 10 * CRC, CirclesType.Demurrage); } } From 9b4122effc3ae6ea181aa01d45c53fdf79823d8f Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Wed, 3 Apr 2024 21:10:25 +0100 Subject: [PATCH 5/8] (test/proxy): use IProxy instead --- src/proxy/Proxy.sol | 2 +- test/lift/ERC20Lift.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy/Proxy.sol b/src/proxy/Proxy.sol index 198db96..c890262 100755 --- a/src/proxy/Proxy.sol +++ b/src/proxy/Proxy.sol @@ -19,7 +19,7 @@ contract Proxy { // to which calls are delegated. // To reduce deployment costs this variable is internal // and needs to be retrieved via `getStorageAt` - address public masterCopy; + address internal masterCopy; /// @dev Constructor function sets address of master copy contract. /// @param _masterCopy Master copy address. diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol index 6214742..1dd4c27 100644 --- a/test/lift/ERC20Lift.t.sol +++ b/test/lift/ERC20Lift.t.sol @@ -52,7 +52,7 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { DemurrageCircles proxyERC20D = DemurrageCircles(lift.ensureERC20(addresses[0], CirclesType.Demurrage)); console.log("proxyERC20D address: ", address(proxyERC20D)); - assertEq(Proxy(payable(address(proxyERC20D))).masterCopy(), address(demurrage)); + assertEq(IProxy(payable(address(proxyERC20D))).masterCopy(), address(demurrage)); // wrap some into demurrage ERC20 // vm.prank(addresses[0]); From 4af9aaaee4194f7a6cf2d1c6271a30f3743331c9 Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Fri, 5 Apr 2024 15:36:32 +0100 Subject: [PATCH 6/8] (circles/demurrage): update R table with cache for proxy storage --- src/circles/Demurrage.sol | 46 +++++++++++++++---- src/circles/DiscountedBalances.sol | 2 +- src/hub/Hub.sol | 4 +- src/lift/DemurrageCircles.sol | 3 +- src/lift/ERC20DiscountedBalances.sol | 9 ++-- src/lift/InflationaryCircles.sol | 7 ++- src/names/NameRegistry.sol | 4 +- src/proxy/Proxy.sol | 18 +++++++- src/treasury/standardVault.sol | 3 +- test/lift/ERC20Lift.t.sol | 68 +++++++++++++++++++++++----- 10 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/circles/Demurrage.sol b/src/circles/Demurrage.sol index bf6577e..3b4a789 100644 --- a/src/circles/Demurrage.sol +++ b/src/circles/Demurrage.sol @@ -121,10 +121,10 @@ contract Demurrage is ICirclesERC1155Errors { * See ../../specifications/TCIP009-demurrage.md for more details. */ int128[15] internal R = [ - int128(18446744073709551616), - int128(18443079296116538654), - int128(18439415246597529027), - int128(18435751925007877736), + int128(18446744073709551616), // 0, ONE_64x64 + int128(18443079296116538654), // 1, GAMMA_64x64 + int128(18439415246597529027), // 2, GAMMA_64x64^2 + int128(18435751925007877736), // 3, etc. int128(18432089331202968517), int128(18428427465038213837), int128(18424766326369054888), @@ -206,11 +206,41 @@ contract Demurrage is ICirclesERC1155Errors { function _calculateDiscountedBalance(uint256 _balance, uint256 _daysDifference) internal view returns (uint256) { if (_daysDifference == 0) { return _balance; - } else if (_daysDifference <= R_TABLE_LOOKUP) { - return Math64x64.mulu(R[_daysDifference], _balance); + } + int128 r = _calculateDemurrageFactor(_daysDifference); + return Math64x64.mulu(r, _balance); + } + + function _calculateDiscountedBalanceAndCache(uint256 _balance, uint256 _daysDifference) + internal + returns (uint256) + { + if (_daysDifference == 0) { + return _balance; + } + int128 r = _calculateDemurrageFactorAndCache(_daysDifference); + return Math64x64.mulu(r, _balance); + } + + function _calculateDemurrageFactor(uint256 _dayDifference) internal view returns (int128) { + if (_dayDifference <= R_TABLE_LOOKUP && R[_dayDifference] != 0) { + return R[_dayDifference]; + } else { + return Math64x64.pow(GAMMA_64x64, _dayDifference); + } + } + + function _calculateDemurrageFactorAndCache(uint256 _dayDifference) internal returns (int128) { + if (_dayDifference <= R_TABLE_LOOKUP) { + if (R[_dayDifference] == 0) { + // for proxy ERC20 contracts, the storage does not contain the R table yet + // so compute it lazily and store it in the table + int128 r = Math64x64.pow(GAMMA_64x64, _dayDifference); + R[_dayDifference] = r; + } + return R[_dayDifference]; } else { - int128 r = Math64x64.pow(GAMMA_64x64, _daysDifference); - return Math64x64.mulu(r, _balance); + return Math64x64.pow(GAMMA_64x64, _dayDifference); } } diff --git a/src/circles/DiscountedBalances.sol b/src/circles/DiscountedBalances.sol index 7654ef3..ea1481c 100644 --- a/src/circles/DiscountedBalances.sol +++ b/src/circles/DiscountedBalances.sol @@ -91,7 +91,7 @@ contract DiscountedBalances is Demurrage { * @dev stores the discounted balances of the accounts privately. * Mapping from Circles identifiers to accounts to the discounted balance. */ - mapping(uint256 => mapping(address => DiscountedBalance)) private discountedBalances; + mapping(uint256 => mapping(address => DiscountedBalance)) public discountedBalances; // /** // * @dev Store a lookup table T(n) for computing issuance. diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 6bb97ff..7cb44d5 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -506,13 +506,15 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { // Public functions - function wrap(address _avatar, uint256 _amount, CirclesType _type) public { + function wrap(address _avatar, uint256 _amount, CirclesType _type) public returns (address) { if (!isHuman(_avatar) && !isGroup(_avatar)) { // Avatar must be human or group. revert CirclesAvatarMustBeRegistered(_avatar, 2); } address erc20Wrapper = liftERC20.ensureERC20(_avatar, _type); safeTransferFrom(msg.sender, erc20Wrapper, toTokenId(_avatar), _amount, ""); + + return erc20Wrapper; } // todo: if we have space, possibly have a wrapBatch function diff --git a/src/lift/DemurrageCircles.sol b/src/lift/DemurrageCircles.sol index cd82e29..a58ff22 100644 --- a/src/lift/DemurrageCircles.sol +++ b/src/lift/DemurrageCircles.sol @@ -5,9 +5,10 @@ import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../hub/IHub.sol"; import "../names/INameRegistry.sol"; +import "../proxy/MasterCopyNonUpgradable.sol"; import "./ERC20DiscountedBalances.sol"; -contract DemurrageCircles is ERC20DiscountedBalances, ERC1155Holder { +contract DemurrageCircles is MasterCopyNonUpgradable, ERC20DiscountedBalances, ERC1155Holder { // Constants // State variables diff --git a/src/lift/ERC20DiscountedBalances.sol b/src/lift/ERC20DiscountedBalances.sol index 2620b33..de6dd36 100644 --- a/src/lift/ERC20DiscountedBalances.sol +++ b/src/lift/ERC20DiscountedBalances.sol @@ -55,7 +55,9 @@ contract ERC20DiscountedBalances is ERC20Permit, Demurrage, IERC20 { } function balanceOf(address _account) external view returns (uint256) { - return balanceOfOnDay(_account, day(block.timestamp)); + uint256 result = balanceOfOnDay(_account, day(block.timestamp)); + require(result > 0, "Turtle"); + return result; } function allowance(address _owner, address _spender) external view returns (uint256) { @@ -92,8 +94,9 @@ contract ERC20DiscountedBalances is ERC20Permit, Demurrage, IERC20 { function _discountAndAddToBalance(address _account, uint256 _value, uint64 _day) internal { DiscountedBalance storage discountedBalance = discountedBalances[_account]; - uint256 newBalance = - _calculateDiscountedBalance(discountedBalance.balance, _day - discountedBalance.lastUpdatedDay) + _value; + uint256 newBalance = _calculateDiscountedBalanceAndCache( + discountedBalance.balance, _day - discountedBalance.lastUpdatedDay + ) + _value; if (newBalance > MAX_VALUE) { // Balance exceeds maximum value. revert CirclesERC1155AmountExceedsMaxUint190(_account, 0, newBalance, 0); diff --git a/src/lift/InflationaryCircles.sol b/src/lift/InflationaryCircles.sol index 1eba6e6..a8e9167 100644 --- a/src/lift/InflationaryCircles.sol +++ b/src/lift/InflationaryCircles.sol @@ -5,9 +5,10 @@ import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../hub/IHub.sol"; import "../names/INameRegistry.sol"; +import "../proxy/MasterCopyNonUpgradable.sol"; import "./ERC20InflationaryBalances.sol"; -contract InflationaryCircles is ERC20InflationaryBalances, ERC1155Holder { +contract InflationaryCircles is MasterCopyNonUpgradable, ERC20InflationaryBalances, ERC1155Holder { // Constants // State variables @@ -121,4 +122,8 @@ contract InflationaryCircles is ERC20InflationaryBalances, ERC1155Holder { { revert CirclesERC1155CannotReceiveBatch(0); } + + function circlesIdentifier() public view returns (uint256) { + return toTokenId(avatar); + } } diff --git a/src/names/NameRegistry.sol b/src/names/NameRegistry.sol index 3e4ce78..20bb3c7 100644 --- a/src/names/NameRegistry.sol +++ b/src/names/NameRegistry.sol @@ -167,10 +167,10 @@ contract NameRegistry is Base58Converter, INameRegistry, INameRegistryErrors, IC uint72 shortName = shortNames[_avatar]; if (shortName == uint72(0)) { string memory base58FullAddress = toBase58(uint256(uint160(_avatar))); - return string(abi.encodePacked("DEFAULT_CIRCLES_NAME_PREFIX", base58FullAddress)); + return string(abi.encodePacked(DEFAULT_CIRCLES_NAME_PREFIX, base58FullAddress)); } string memory base58ShortName = toBase58(uint256(shortName)); - return string(abi.encodePacked("DEFAULT_CIRCLES_NAME_PREFIX", base58ShortName)); + return string(abi.encodePacked(DEFAULT_CIRCLES_NAME_PREFIX, base58ShortName)); } function symbol(address _avatar) external view mustBeRegistered(_avatar, 2) returns (string memory) { diff --git a/src/proxy/Proxy.sol b/src/proxy/Proxy.sol index c890262..09888c6 100755 --- a/src/proxy/Proxy.sol +++ b/src/proxy/Proxy.sol @@ -19,14 +19,14 @@ contract Proxy { // to which calls are delegated. // To reduce deployment costs this variable is internal // and needs to be retrieved via `getStorageAt` - address internal masterCopy; + address public masterCopy; /// @dev Constructor function sets address of master copy contract. /// @param _masterCopy Master copy address. constructor(address _masterCopy) { if (_masterCopy == address(0)) { // Invalid master copy address provided - revert() ; + revert(); } masterCopy = _masterCopy; } @@ -42,6 +42,20 @@ contract Proxy { // -- internal functions + // /// @dev Fallback function forwards all transactions and + // /// returns all received return data. + // function _fallback() internal { + // // solium-disable-next-line security/no-inline-assembly + // assembly { + // let mc := sload(0) + // calldatacopy(0, 0, calldatasize()) + // let success := delegatecall(gas(), mc, 0, calldatasize(), 0, 0) + // returndatacopy(0, 0, returndatasize()) + // if eq(success, 0) { revert(0, returndatasize()) } + // return(0, returndatasize()) + // } + // } + /// @dev Fallback function forwards all transactions and /// returns all received return data. function _fallback() internal { diff --git a/src/treasury/standardVault.sol b/src/treasury/standardVault.sol index df598b2..c4b84f0 100644 --- a/src/treasury/standardVault.sol +++ b/src/treasury/standardVault.sol @@ -4,9 +4,10 @@ pragma solidity >=0.8.13; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "../errors/Errors.sol"; import "../hub/IHub.sol"; +import "../proxy/MasterCopyNonUpgradable.sol"; import "./IStandardVault.sol"; -contract StandardVault is ERC1155Holder, IStandardVault, ICirclesErrors { +contract StandardVault is MasterCopyNonUpgradable, ERC1155Holder, IStandardVault, ICirclesErrors { // State variables /** diff --git a/test/lift/ERC20Lift.t.sol b/test/lift/ERC20Lift.t.sol index 1dd4c27..f712054 100644 --- a/test/lift/ERC20Lift.t.sol +++ b/test/lift/ERC20Lift.t.sol @@ -4,6 +4,7 @@ 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"; @@ -33,29 +34,72 @@ contract ERC20LiftTest is Test, TimeCirclesSetup, HumanRegistration { // Tests function testERC20Wrap() public { - // register Alice + // register Alice and Bob vm.prank(addresses[0]); hub.registerHumanUnrestricted(); + vm.prank(addresses[1]); + hub.registerHumanUnrestricted(); + + // Alice registers short name + vm.startPrank(addresses[0]); + mockDeployment.nameRegistry().registerShortName(); + vm.stopPrank(); // skip time and mint skipTime(14 days); vm.prank(addresses[0]); hub.personalMintWithoutV1Check(); + vm.prank(addresses[1]); + hub.personalMintWithoutV1Check(); + + uint256 aliceBalance = hub.balanceOf(addresses[0], uint256(uint160(addresses[0]))); + console.log("Alice balance: ", aliceBalance); // test the master contracts in Lift - ERC20Lift lift = mockDeployment.erc20Lift(); - DemurrageCircles demurrage = mockDeployment.mastercopyDemurrageCircles(); - address demurrageMasterCopy = lift.masterCopyERC20Wrapper(uint256(CirclesType.Demurrage)); - assertEq(demurrageMasterCopy, address(demurrage)); + // ERC20Lift lift = mockDeployment.erc20Lift(); + // DemurrageCircles demurrage = mockDeployment.mastercopyDemurrageCircles(); + // address demurrageMasterCopy = lift.masterCopyERC20Wrapper(uint256(CirclesType.Demurrage)); + // assertEq(demurrageMasterCopy, address(demurrage)); + + // console.log("hub address: ", address(hub)); + + // DemurrageCircles proxyERC20D = DemurrageCircles(lift.ensureERC20(addresses[0], CirclesType.Demurrage)); + // console.log("proxyERC20D address: ", address(proxyERC20D)); + // assertEq(IProxy(payable(address(proxyERC20D))).masterCopy(), address(demurrage)); + + // wrap some into demurrage ERC20 of Alice by Alice + vm.prank(addresses[0]); + DemurrageCircles aliceERC20 = DemurrageCircles(hub.wrap(addresses[0], 10 * CRC, CirclesType.Demurrage)); + assertEq(aliceERC20.balanceOf(addresses[0]), 10 * CRC); + + // Give Bob some Alice CRC, so he can wrap them too + vm.prank(addresses[0]); + hub.safeTransferFrom(addresses[0], addresses[1], uint256(uint160(addresses[0])), 5 * CRC, ""); + vm.prank(addresses[1]); + hub.wrap(addresses[0], 5 * CRC, CirclesType.Demurrage); + // assert Bob has 5 CRC in Alice's ERC20 + assertEq(aliceERC20.balanceOf(addresses[1]), 5 * CRC); + + // now test wrapping by simply sending ERC1155 to the ERC20 wrapper + vm.prank(addresses[0]); + hub.safeTransferFrom(addresses[0], address(aliceERC20), uint256(uint160(addresses[0])), 5 * CRC, ""); + // assert Alice has 10 + 5 = 15 CRC in her ERC20 + assertEq(aliceERC20.balanceOf(addresses[0]), 15 * CRC); + // Alice wrapped 15 CRC, and gave 5 CRC to Bob + assertEq(hub.balanceOf(addresses[0], uint256(uint160(addresses[0]))), aliceBalance - 20 * CRC); - console.log("hub address: ", address(hub)); + // somewhat cheekily test here that the demurrage works in ERC20 too + // todo: split this out into proper unit tests, rather than stories - DemurrageCircles proxyERC20D = DemurrageCircles(lift.ensureERC20(addresses[0], CirclesType.Demurrage)); - console.log("proxyERC20D address: ", address(proxyERC20D)); - assertEq(IProxy(payable(address(proxyERC20D))).masterCopy(), address(demurrage)); + (uint192 balance, uint64 lastUpdatedDay) = aliceERC20.discountedBalances(addresses[0]); + console.log("ERC1155 balance: ", hub.balanceOf(addresses[0], uint256(uint160(addresses[0])))); + console.log("balance: ", balance); + console.log("lastUpdatedDay: ", lastUpdatedDay); - // wrap some into demurrage ERC20 - // vm.prank(addresses[0]); - // hub.wrap(addresses[0], 10 * CRC, CirclesType.Demurrage); + // skip time + skipTime(2 days); + // assert Alice has 15 CRC in her ERC20 + // 2 days, 15 * (0.9998013320086...)^2 = 14.994040552292530832 (rounded down) + assertEq(aliceERC20.balanceOf(addresses[0]), 14994040552292530832); } } From f70fc57af2ff6afe896ca06d39c8a5a62154d53d Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Fri, 5 Apr 2024 18:21:38 +0100 Subject: [PATCH 7/8] (test/demurrage): asserted the computed values match within DUST the python table --- test/circles/Demurrage.t.sol | 36 ++++++++++++++++++++++++++++++++++ test/circles/MockDemurrage.sol | 24 +++++++++++++++++++++++ test/utils/Approximation.sol | 5 +++++ 3 files changed, 65 insertions(+) create mode 100644 test/circles/Demurrage.t.sol create mode 100644 test/circles/MockDemurrage.sol diff --git a/test/circles/Demurrage.t.sol b/test/circles/Demurrage.t.sol new file mode 100644 index 0000000..fa292e2 --- /dev/null +++ b/test/circles/Demurrage.t.sol @@ -0,0 +1,36 @@ +// 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 "../setup/TimeCirclesSetup.sol"; +import "../utils/Approximation.sol"; +import "./MockDemurrage.sol"; + +contract DemurrageTest is Test, TimeCirclesSetup, Approximation { + // State variables + + MockDemurrage public demurrage; + + // Setup + + function setUp() public { + // Set time in 2021 + startTime(); + + demurrage = new MockDemurrage(); + } + + // Tests + + function testDemurrageFactor() public { + for (uint256 i = 0; i <= demurrage.rLength(); i++) { + assertTrue( + relativeApproximatelyEqual( + uint256(int256(Math64x64.pow(demurrage.gamma_64x64(), i))), uint256(int256(demurrage.r(i))), DUST + ) + ); + } + } +} diff --git a/test/circles/MockDemurrage.sol b/test/circles/MockDemurrage.sol new file mode 100644 index 0000000..9ed1af5 --- /dev/null +++ b/test/circles/MockDemurrage.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/circles/Demurrage.sol"; + +contract MockDemurrage is Demurrage { + // External functions + + function setInflationDayZero(uint256 _inflationDayZero) external { + inflationDayZero = _inflationDayZero; + } + + function gamma_64x64() external view returns (int128) { + return GAMMA_64x64; + } + + function r(uint256 _i) external view returns (int128) { + return R[_i]; + } + + function rLength() external view returns (uint256) { + return R_TABLE_LOOKUP; + } +} diff --git a/test/utils/Approximation.sol b/test/utils/Approximation.sol index d80a091..bd503fd 100644 --- a/test/utils/Approximation.sol +++ b/test/utils/Approximation.sol @@ -11,6 +11,11 @@ contract Approximation { // 1% in 64x64 fixed point: integer approximation of 2**64 / 100 int128 internal constant ONE_PERCENT = int128(184467440737095516); + // Dust in 64x64 fixed point: integer approximation of 2**64 / 10**18 + int128 internal constant DUST = int128(18); + + // Public functions + function approximatelyEqual(uint256 _a, uint256 _b, uint256 _epsilon) public pure returns (bool) { return _a > _b ? _a - _b <= _epsilon : _b - _a <= _epsilon; } From e7b4d3b02cde43ba256e89f13342f38950a7314f Mon Sep 17 00:00:00 2001 From: Benjamin Bollen Date: Fri, 5 Apr 2024 19:26:58 +0100 Subject: [PATCH 8/8] (Demurrage): always compute R, either in constructor or lazily in ERC20 --- src/circles/Demurrage.sol | 50 ++++++++++++++++++---------- src/lift/ERC20DiscountedBalances.sol | 4 +-- test/circles/Demurrage.t.sol | 24 ++++++++++++- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/circles/Demurrage.sol b/src/circles/Demurrage.sol index 3b4a789..d5ef7e3 100644 --- a/src/circles/Demurrage.sol +++ b/src/circles/Demurrage.sol @@ -96,6 +96,11 @@ contract Demurrage is ICirclesERC1155Errors { /** * @dev Store a lookup table T(n) for computing issuance. + * T is only accessed for minting in Hub.sol, so it is initialized in + * storage of Hub.sol during the constructor, by copying these values. + * (It is not properly intialized in a ERC20 Proxy contract, but we never need + * to access it there, so it is not a problem - it is only initialized in the + * storage of the mastercopy during deployment.) * See ../../specifications/TCIP009-demurrage.md for more details. */ int128[15] internal T = [ @@ -117,26 +122,35 @@ contract Demurrage is ICirclesERC1155Errors { ]; /** - * @dev Store a lookup table R(n) for computing issuance. + * @dev Store a lookup table R(n) for computing issuance and demurrage. + * This table is computed in the constructor of Hub.sol and mastercopy deployments, + * and lazily computed in the ERC20 Demurrage proxy contracts, then cached into their storage. + * The non-trivial situation for R(n) (vs T(n)) is that R is accessed + * from the ERC20 Demurrage proxy contracts, so their storage will not yet + * have been initialized with the constructor. (Counter to T which is only + * accessed for minting in Hub.sol, and as such initialized in the constructor + * of Hub.sol by Solidity by copying the python calculated values stored above.) + * + * Computing R in contract is done with .64bits precision, whereas the python computed + * table is slightly more accurate, but equal within dust (10^-18). See unit tests. + * However, we want to ensure that Hub.sol and the ERC20 Demurrage proxy contracts + * use the exact same R values (even if the difference would not matter). + * So for R we rely on the in-contract computed values. + * In the unit tests, the table of python computed values is stored in HIGHER_ACCURACY_R, + * and matched against the solidity computed values. * See ../../specifications/TCIP009-demurrage.md for more details. */ - int128[15] internal R = [ - int128(18446744073709551616), // 0, ONE_64x64 - int128(18443079296116538654), // 1, GAMMA_64x64 - int128(18439415246597529027), // 2, GAMMA_64x64^2 - int128(18435751925007877736), // 3, etc. - int128(18432089331202968517), - int128(18428427465038213837), - int128(18424766326369054888), - int128(18421105915050961582), - int128(18417446230939432544), - int128(18413787273889995104), - int128(18410129043758205300), - int128(18406471540399647861), - int128(18402814763669936209), - int128(18399158713424712450), - int128(18395503389519647372) - ]; + int128[15] internal R; + + // Constructor + + constructor() { + // we need to fill the R table upon construction so that + // in Hub.sol personalMint has the R table available + for (uint8 i = 0; i <= R_TABLE_LOOKUP; i++) { + R[i] = Math64x64.pow(GAMMA_64x64, i); + } + } // Public functions diff --git a/src/lift/ERC20DiscountedBalances.sol b/src/lift/ERC20DiscountedBalances.sol index de6dd36..12c6103 100644 --- a/src/lift/ERC20DiscountedBalances.sol +++ b/src/lift/ERC20DiscountedBalances.sol @@ -55,9 +55,7 @@ contract ERC20DiscountedBalances is ERC20Permit, Demurrage, IERC20 { } function balanceOf(address _account) external view returns (uint256) { - uint256 result = balanceOfOnDay(_account, day(block.timestamp)); - require(result > 0, "Turtle"); - return result; + return balanceOfOnDay(_account, day(block.timestamp)); } function allowance(address _owner, address _spender) external view returns (uint256) { diff --git a/test/circles/Demurrage.t.sol b/test/circles/Demurrage.t.sol index fa292e2..0586f75 100644 --- a/test/circles/Demurrage.t.sol +++ b/test/circles/Demurrage.t.sol @@ -13,6 +13,28 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation { MockDemurrage public demurrage; + /** + * @dev Store a lookup table R(n) for computing issuance. + * See ../../specifications/TCIP009-demurrage.md for more details. + */ + uint256[15] internal HIGHER_ACCURACY_R = [ + uint256(18446744073709551616), // 0, ONE_64x64 + uint256(18443079296116538654), // 1, GAMMA_64x64 + uint256(18439415246597529027), // 2, GAMMA_64x64^2 + uint256(18435751925007877736), // 3, etc. + uint256(18432089331202968517), + uint256(18428427465038213837), + uint256(18424766326369054888), + uint256(18421105915050961582), + uint256(18417446230939432544), + uint256(18413787273889995104), + uint256(18410129043758205300), + uint256(18406471540399647861), + uint256(18402814763669936209), + uint256(18399158713424712450), + uint256(18395503389519647372) + ]; + // Setup function setUp() public { @@ -28,7 +50,7 @@ contract DemurrageTest is Test, TimeCirclesSetup, Approximation { for (uint256 i = 0; i <= demurrage.rLength(); i++) { assertTrue( relativeApproximatelyEqual( - uint256(int256(Math64x64.pow(demurrage.gamma_64x64(), i))), uint256(int256(demurrage.r(i))), DUST + uint256(int256(Math64x64.pow(demurrage.gamma_64x64(), i))), HIGHER_ACCURACY_R[i], DUST ) ); }