From a02bb0a48cb78c815ec3a2f3bd351f2efc3869d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Evaldas=20Lato=C5=A1kinas?= <34982762+elatoskinas@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:01:19 +0200 Subject: [PATCH] Add EnumerableMapAddresses library (#13428) * feat: add EnumerableMapAddresses library * refactor: remove redundant helpers --- contracts/.changeset/cyan-apes-perform.md | 5 + contracts/gas-snapshots/shared.gas-snapshot | 15 ++ .../enumerable/EnumerableMapAddresses.sol | 140 ++++++++++++++ .../enumerable/EnumerableMapAddresses.t.sol | 176 ++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 contracts/.changeset/cyan-apes-perform.md create mode 100644 contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol create mode 100644 contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol diff --git a/contracts/.changeset/cyan-apes-perform.md b/contracts/.changeset/cyan-apes-perform.md new file mode 100644 index 00000000000..b35f80a9814 --- /dev/null +++ b/contracts/.changeset/cyan-apes-perform.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': minor +--- + +#added EnumerableMapAddresses shared lib for AddressToAddress and AddressToBytes32 maps diff --git a/contracts/gas-snapshots/shared.gas-snapshot b/contracts/gas-snapshots/shared.gas-snapshot index 2f46629eb63..ed0620b1276 100644 --- a/contracts/gas-snapshots/shared.gas-snapshot +++ b/contracts/gas-snapshots/shared.gas-snapshot @@ -42,6 +42,21 @@ CallWithExactGas__callWithExactGasSafeReturnData:test_NoContractReverts() (gas: CallWithExactGas__callWithExactGasSafeReturnData:test_NoGasForCallExactCheckReverts() (gas: 16139) CallWithExactGas__callWithExactGasSafeReturnData:test_NotEnoughGasForCallReverts() (gas: 16547) CallWithExactGas__callWithExactGasSafeReturnData:test_callWithExactGasSafeReturnData_ThrowOOGError_Revert() (gas: 36752) +EnumerableMapAddresses_at:testAtSuccess() (gas: 95001) +EnumerableMapAddresses_at:testBytes32AtSuccess() (gas: 94770) +EnumerableMapAddresses_contains:testBytes32ContainsSuccess() (gas: 93518) +EnumerableMapAddresses_contains:testContainsSuccess() (gas: 93696) +EnumerableMapAddresses_get:testBytes32GetSuccess() (gas: 94249) +EnumerableMapAddresses_get:testGetSuccess() (gas: 94436) +EnumerableMapAddresses_get_errorMessage:testGetErrorMessageSuccess() (gas: 94477) +EnumerableMapAddresses_length:testBytes32LengthSuccess() (gas: 72404) +EnumerableMapAddresses_length:testLengthSuccess() (gas: 72582) +EnumerableMapAddresses_remove:testBytes32RemoveSuccess() (gas: 73408) +EnumerableMapAddresses_remove:testRemoveSuccess() (gas: 73686) +EnumerableMapAddresses_set:testBytes32SetSuccess() (gas: 94496) +EnumerableMapAddresses_set:testSetSuccess() (gas: 94685) +EnumerableMapAddresses_tryGet:testBytes32TryGetSuccess() (gas: 94604) +EnumerableMapAddresses_tryGet:testTryGetSuccess() (gas: 94864) OpStackBurnMintERC677_constructor:testConstructorSuccess() (gas: 1743649) OpStackBurnMintERC677_interfaceCompatibility:testBurnCompatibility() (gas: 298649) OpStackBurnMintERC677_interfaceCompatibility:testMintCompatibility() (gas: 137957) diff --git a/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol b/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol new file mode 100644 index 00000000000..6fbd37c60d7 --- /dev/null +++ b/contracts/src/v0.8/shared/enumerable/EnumerableMapAddresses.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {EnumerableMap} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/structs/EnumerableMap.sol"; + +// TODO: the lib can be replaced with OZ v5.1 post-upgrade, which has AddressToAddressMap and AddressToBytes32Map +library EnumerableMapAddresses { + using EnumerableMap for EnumerableMap.UintToAddressMap; + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + + struct AddressToAddressMap { + EnumerableMap.UintToAddressMap _inner; + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function set(AddressToAddressMap storage map, address key, address value) internal returns (bool) { + return map._inner.set(uint256(uint160(key)), value); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function remove(AddressToAddressMap storage map, address key) internal returns (bool) { + return map._inner.remove(uint256(uint160(key))); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function contains(AddressToAddressMap storage map, address key) internal view returns (bool) { + return map._inner.contains(uint256(uint160(key))); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function length(AddressToAddressMap storage map) internal view returns (uint256) { + return map._inner.length(); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function at(AddressToAddressMap storage map, uint256 index) internal view returns (address, address) { + (uint256 key, address value) = map._inner.at(index); + return (address(uint160(key)), value); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function tryGet(AddressToAddressMap storage map, address key) internal view returns (bool, address) { + return map._inner.tryGet(uint256(uint160(key))); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function get(AddressToAddressMap storage map, address key) internal view returns (address) { + return map._inner.get(uint256(uint160(key))); + } + + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function get( + AddressToAddressMap storage map, + address key, + string memory errorMessage + ) internal view returns (address) { + return map._inner.get(uint256(uint160(key)), errorMessage); + } + + // AddressToBytes32Map + + struct AddressToBytes32Map { + EnumerableMap.Bytes32ToBytes32Map _inner; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function set(AddressToBytes32Map storage map, address key, bytes32 value) internal returns (bool) { + return map._inner.set(bytes32(uint256(uint160(key))), value); + } + + /** + * @dev Removes a value from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function remove(AddressToBytes32Map storage map, address key) internal returns (bool) { + return map._inner.remove(bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function contains(AddressToBytes32Map storage map, address key) internal view returns (bool) { + return map._inner.contains(bytes32(uint256(uint160(key)))); + } + + /** + * @dev Returns the number of elements in the map. O(1). + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function length(AddressToBytes32Map storage map) internal view returns (uint256) { + return map._inner.length(); + } + + /** + * @dev Returns the element stored at position `index` in the map. O(1). + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function at(AddressToBytes32Map storage map, uint256 index) internal view returns (address, bytes32) { + (bytes32 key, bytes32 value) = map._inner.at(index); + return (address(uint160(uint256(key))), value); + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function tryGet(AddressToBytes32Map storage map, address key) internal view returns (bool, bytes32) { + (bool success, bytes32 value) = map._inner.tryGet(bytes32(uint256(uint160(key)))); + return (success, value); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore + function get(AddressToBytes32Map storage map, address key) internal view returns (bytes32) { + return map._inner.get(bytes32(uint256(uint160(key)))); + } +} diff --git a/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol b/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol new file mode 100644 index 00000000000..900c546f66d --- /dev/null +++ b/contracts/src/v0.8/shared/test/enumerable/EnumerableMapAddresses.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {BaseTest} from "../BaseTest.t.sol"; +import {EnumerableMapAddresses} from "../../enumerable/EnumerableMapAddresses.sol"; + +contract EnumerableMapAddressesTest is BaseTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + EnumerableMapAddresses.AddressToAddressMap internal s_addressToAddressMap; + EnumerableMapAddresses.AddressToBytes32Map internal s_addressToBytes32Map; + + bytes32 internal constant MOCK_BYTES32_VALUE = bytes32(uint256(42)); + + function setUp() public virtual override { + BaseTest.setUp(); + } +} + +contract EnumerableMapAddresses_set is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testSetSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + assertTrue(!s_addressToAddressMap.set(address(this), address(this))); + } + + function testBytes32SetSuccess() public { + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.contains(address(this))); + assertTrue(!s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + } +} + +contract EnumerableMapAddresses_remove is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testRemoveSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.remove(address(this))); + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(!s_addressToAddressMap.remove(address(this))); + } + + function testBytes32RemoveSuccess() public { + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.remove(address(this))); + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(!s_addressToBytes32Map.remove(address(this))); + } +} + +contract EnumerableMapAddresses_contains is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testContainsSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + } + + function testBytes32ContainsSuccess() public { + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.contains(address(this))); + } +} + +contract EnumerableMapAddresses_length is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testLengthSuccess() public { + assertTrue(s_addressToAddressMap.length() == 0); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.length() == 1); + assertTrue(s_addressToAddressMap.remove(address(this))); + assertTrue(s_addressToAddressMap.length() == 0); + } + + function testBytes32LengthSuccess() public { + assertTrue(s_addressToBytes32Map.length() == 0); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.length() == 1); + assertTrue(s_addressToBytes32Map.remove(address(this))); + assertTrue(s_addressToBytes32Map.length() == 0); + } +} + +contract EnumerableMapAddresses_at is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testAtSuccess() public { + assertTrue(s_addressToAddressMap.length() == 0); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.length() == 1); + (address key, address value) = s_addressToAddressMap.at(0); + assertTrue(key == address(this)); + assertTrue(value == address(this)); + } + + function testBytes32AtSuccess() public { + assertTrue(s_addressToBytes32Map.length() == 0); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.length() == 1); + (address key, bytes32 value) = s_addressToBytes32Map.at(0); + assertTrue(key == address(this)); + assertTrue(value == MOCK_BYTES32_VALUE); + } +} + +contract EnumerableMapAddresses_tryGet is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testTryGetSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + (bool success, address value) = s_addressToAddressMap.tryGet(address(this)); + assertTrue(success); + assertTrue(value == address(this)); + } + + function testBytes32TryGetSuccess() public { + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.contains(address(this))); + (bool success, bytes32 value) = s_addressToBytes32Map.tryGet(address(this)); + assertTrue(success); + assertTrue(value == MOCK_BYTES32_VALUE); + } +} + +contract EnumerableMapAddresses_get is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testGetSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.get(address(this)) == address(this)); + } + + function testBytes32GetSuccess() public { + assertTrue(!s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.set(address(this), MOCK_BYTES32_VALUE)); + assertTrue(s_addressToBytes32Map.contains(address(this))); + assertTrue(s_addressToBytes32Map.get(address(this)) == MOCK_BYTES32_VALUE); + } +} + +contract EnumerableMapAddresses_get_errorMessage is EnumerableMapAddressesTest { + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToBytes32Map; + using EnumerableMapAddresses for EnumerableMapAddresses.AddressToAddressMap; + + function testGetErrorMessageSuccess() public { + assertTrue(!s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.set(address(this), address(this))); + assertTrue(s_addressToAddressMap.contains(address(this))); + assertTrue(s_addressToAddressMap.get(address(this), "EnumerableMapAddresses: nonexistent key") == address(this)); + } +}