From c1a4ebad3804954c4074c7e6eaba162f1a1a3586 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 10 Oct 2024 09:03:05 +0000 Subject: [PATCH 01/13] Add EnumerableRoles and tests --- .gitmodules | 18 +- lib/devtools | 2 +- lib/forge-std | 2 +- lib/multicaller | 1 + lib/openzeppelin-contracts | 2 +- lib/openzeppelin-contracts-upgradeable | 2 +- lib/solady | 2 +- lib/soledge | 1 + remappings.txt | 2 + src/EnumerableRoles.sol | 153 +++++ test/EnumerableRoles.t.sol | 116 ++++ test/mocks/MockEnumerableRoles.sol | 26 + test/utils/Brutalizer.sol | 885 +++++++++++++++++++++++++ test/utils/SoladyTest.sol | 12 + test/utils/TestPlus.sol | 686 +++++++++++++++++++ 15 files changed, 1899 insertions(+), 11 deletions(-) create mode 160000 lib/multicaller create mode 160000 lib/soledge create mode 100644 src/EnumerableRoles.sol create mode 100644 test/EnumerableRoles.t.sol create mode 100644 test/mocks/MockEnumerableRoles.sol create mode 100644 test/utils/Brutalizer.sol create mode 100644 test/utils/SoladyTest.sol create mode 100644 test/utils/TestPlus.sol diff --git a/.gitmodules b/.gitmodules index a4badc1..17aacf0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/solady"] - path = lib/solady - url = https://github.com/Vectorized/solady [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/clustersxyz/LayerZero-v2 @@ -17,3 +11,15 @@ path = lib/devtools url = https://github.com/0xfoobar/devtools branch = patch-1 +[submodule "lib/soledge"] + path = lib/soledge + url = https://github.com/Vectorized/soledge +[submodule "lib/multicaller"] + path = lib/multicaller + url = https://github.com/vectorized/multicaller +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/devtools b/lib/devtools index f2d2684..cb9dcb9 160000 --- a/lib/devtools +++ b/lib/devtools @@ -1 +1 @@ -Subproject commit f2d2684f9b57ccfc10afcb9f4290c12bc12162f0 +Subproject commit cb9dcb9f5982f9f086f0ce9fc0b5fdf1f60f84b0 diff --git a/lib/forge-std b/lib/forge-std index 6e05729..bf66061 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6e05729b76f1ae0d437e74951aef1ca987788ab3 +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 diff --git a/lib/multicaller b/lib/multicaller new file mode 160000 index 0000000..356350c --- /dev/null +++ b/lib/multicaller @@ -0,0 +1 @@ +Subproject commit 356350c2954d9e117ec839be37bddcdc773e04ac diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dc62599..72c152d 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dc625992575ecb3089acc35f5475bedfcb7e6be3 +Subproject commit 72c152dc1c41f23d7c504e175f5b417fccc89426 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 1bf98f4..22489db 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 1bf98f4edcf9d9e757314a49354ea98a3a771db4 +Subproject commit 22489db15621b9a42ebddb1facade6962034e9b9 diff --git a/lib/solady b/lib/solady index 74769c2..efd064c 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 74769c21b5759e897e90fb25026f24d889fe4b26 +Subproject commit efd064c9cd8f75ee0eff5b31898642581bbc97fd diff --git a/lib/soledge b/lib/soledge new file mode 160000 index 0000000..641f4fc --- /dev/null +++ b/lib/soledge @@ -0,0 +1 @@ +Subproject commit 641f4fc39f6ca9b387115985a099b4dfd26135fb diff --git a/remappings.txt b/remappings.txt index 0a67d8b..b74bc46 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,8 @@ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ solmate/=lib/solmate/src/ solady/=lib/solady/src/ +multicaller/=lib/multicaller/src/ +soledge/=lib/soledge/src/ clusters/=src/ solidity-bytes-utils/=lib/LayerZero-v2/oapp/node_modules/solidity-bytes-utils/ @openzeppelin/contracts/=lib/LayerZero-v2/oapp/node_modules/@openzeppelin/contracts/ diff --git a/src/EnumerableRoles.sol b/src/EnumerableRoles.sol new file mode 100644 index 0000000..2de408b --- /dev/null +++ b/src/EnumerableRoles.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol"; + +/// @title EnumerableRoles +/// @notice Enumerable roles mixin that does not require inheritance from any specific ownable. +contract EnumerableRoles { + using EnumerableSetLib for *; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The storage struct for the contract. + struct EnumerableRolesStorage { + // Mapping of `role` to a set of addresses with the role. + mapping(uint8 => EnumerableSetLib.AddressSet) holders; + // Mapping of an address to a set of roles it has. + mapping(address => EnumerableSetLib.Uint8Set) roles; + } + + /// @dev Returns the storage struct for the contract. + function _getEnumerableRolesStorage() internal pure returns (EnumerableRolesStorage storage $) { + assembly ("memory-safe") { + // `uint72(bytes9(keccak256("Clusters.EnumerableRolesStorage")))`. + $.slot := 0x214b1bb45059b2b86 // Truncate to 9 bytes to reduce bytecode size. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The status of `role` for `user` has been set to `active`. + event RoleSet(address user, uint8 role, bool active); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Not authorized to set the role. + error SetRoleUnauthorized(); + + /// @dev Not authorized, as the caller does not have the role. + error CheckRoleUnauthorized(); + + /// @dev The role is greater than `MAX_ROLE`. + error InvalidRole(); + + /// @dev The user cannot be the zero address. + error UserIsZeroAddress(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC WRITE FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the status of `role` for `user`. + function setRole(address user, uint8 role, bool active) public payable virtual { + if (msg.sender != _thisOwner()) revert SetRoleUnauthorized(); + _setRole(user, role, active); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns all the holders of `role`. + function roleHolders(uint8 role) public view virtual returns (address[] memory) { + return _getEnumerableRolesStorage().holders[role].values(); + } + + /// @dev Returns the number of holders with `role`. + function roleHoldersCount(uint8 role) public view virtual returns (uint256) { + return _getEnumerableRolesStorage().holders[role].length(); + } + + /// @dev Returns the holder of `role` at index `i`. + function roleHoldersAt(uint8 role, uint256 i) public view virtual returns (address) { + return _getEnumerableRolesStorage().holders[role].at(i); + } + + /// @dev Returns the roles of `user`. + function rolesOf(address user) public view virtual returns (uint8[] memory) { + return _getEnumerableRolesStorage().roles[user].values(); + } + + /// @dev Returns if `user` has `role` set to active. + function hasRole(address user, uint8 role) public view virtual returns (bool) { + return _getEnumerableRolesStorage().roles[user].contains(role); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Guards a function such that the caller must have `role`. + modifier onlyRole(uint8 role) virtual { + _checkRole(role); + _; + } + + /// @dev Guards a function such that the caller must be the contact owner or have `role`. + modifier onlyOwnerOrRole(uint8 role) virtual { + _checkOwnerOrRole(role); + _; + } + + /// @dev Requires that the caller is the contract owner or has `role`. + function _checkOwnerOrRole(uint8 role) internal virtual { + if (msg.sender != _thisOwner()) { + if (!hasRole(msg.sender, role)) revert CheckRoleUnauthorized(); + } + } + + /// @dev Requires that the caller has `role`. + function _checkRole(uint8 role) internal virtual { + if (!hasRole(msg.sender, role)) revert CheckRoleUnauthorized(); + } + + /// @dev Sets the role without authorization checks. + function _setRole(address user, uint8 role, bool active) internal virtual { + if (role > _maxRole()) revert InvalidRole(); + if (user == address(0)) revert UserIsZeroAddress(); + EnumerableRolesStorage storage $ = _getEnumerableRolesStorage(); + if (active) { + $.roles[user].add(role); + $.holders[role].add(user); + } else { + $.roles[user].remove(role); + $.holders[role].remove(user); + } + emit RoleSet(user, role, active); + } + + /// @dev Returns the owner of the contract. + function _thisOwner() internal view virtual returns (address result) { + assembly ("memory-safe") { + mstore(0x00, 0x8da5cb5b) // `owner()`. + result := + mul(mload(0x00), and(gt(returndatasize(), 0x1f), staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20))) + } + } + + /// @dev Returns the maximum valid role. + function _maxRole() internal view virtual returns (uint256 result) { + assembly ("memory-safe") { + mstore(0x00, 0xd24f19d5) // `MAX_ROLE()`. + result := + mul(mload(0x00), and(gt(returndatasize(), 0x1f), staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20))) + } + } +} diff --git a/test/EnumerableRoles.t.sol b/test/EnumerableRoles.t.sol new file mode 100644 index 0000000..3ae3bf5 --- /dev/null +++ b/test/EnumerableRoles.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {LibSort} from "solady/utils/LibSort.sol"; +import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol"; +import "./utils/SoladyTest.sol"; +import "./mocks/MockEnumerableRoles.sol"; + +contract EnumerableRolesTest is SoladyTest { + using DynamicArrayLib for *; + + MockEnumerableRoles internal enumerableRoles; + + function setUp() public { + enumerableRoles = new MockEnumerableRoles(); + } + + function testSetAndGetMaxRole(uint256 value) public { + enumerableRoles.setMaxRole(value); + assertEq(enumerableRoles.maxRole(), value); + } + + function testSetAndGetOwner(address value) public { + enumerableRoles.setOwner(value); + assertEq(enumerableRoles.thisOwner(), value); + } + + function testSetAndGetRoles(bytes32) public { + address user0; + address user1; + do { + user0 = _randomNonZeroAddress(); + user1 = _randomNonZeroAddress(); + } while (user0 == address(0) || user1 == address(0) || user0 == user1); + _testSetAndGetRoles(user0, user1, _sampleRoles(), _sampleRoles()); + } + + function testSetAndGetRoles() public { + uint256[] memory allRoles = DynamicArrayLib.malloc(256); + unchecked { + for (uint256 i; i < 256; ++i) { + allRoles.set(i, i); + } + } + _testSetAndGetRoles(address(1), address(2), allRoles, allRoles); + } + + function _testSetAndGetRoles(address user0, address user1, uint256[] memory user0Roles, uint256[] memory user1Roles) + internal + { + enumerableRoles.setMaxRole(255); + enumerableRoles.setOwner(address(this)); + unchecked { + for (uint256 i; i != user0Roles.length; ++i) { + enumerableRoles.setRole(user0, uint8(user0Roles.get(i)), true); + } + for (uint256 i; i != user1Roles.length; ++i) { + enumerableRoles.setRole(user1, uint8(user1Roles.get(i)), true); + } + _checkRoles(user0, user0Roles); + _checkRoles(user1, user1Roles); + if (_randomChance(32)) { + for (uint256 i; i < 256; ++i) { + if (!_randomChance(8)) continue; + uint8 role = uint8(i); + DynamicArrayLib.DynamicArray memory expected; + if (user0Roles.contains(role)) expected.p(user0); + if (user1Roles.contains(role)) expected.p(user1); + LibSort.sort(expected.data); + address[] memory roleHolders = enumerableRoles.roleHolders(role); + LibSort.sort(roleHolders); + assertEq(abi.encodePacked(expected.data), abi.encodePacked(roleHolders)); + } + } + for (uint256 i; i != user0Roles.length; ++i) { + enumerableRoles.setRole(user0, uint8(user0Roles.get(i)), false); + } + for (uint256 i; i != user1Roles.length; ++i) { + enumerableRoles.setRole(user1, uint8(user1Roles.get(i)), false); + } + assertEq(enumerableRoles.rolesOf(user0).length, 0); + assertEq(enumerableRoles.rolesOf(user1).length, 0); + if (_randomChance(32)) { + for (uint256 i; i < 256; ++i) { + uint8 role = uint8(i); + assertEq(enumerableRoles.roleHolders(role).length, 0); + } + } + } + } + + function _checkRoles(address user, uint256[] memory sampledRoles) internal view { + uint8[] memory roles = enumerableRoles.rolesOf(user); + LibSort.sort(_toUint256Array(roles)); + sampledRoles = LibSort.copy(sampledRoles); + LibSort.sort(sampledRoles); + LibSort.uniquifySorted(sampledRoles); + assertEq(_toUint256Array(roles), sampledRoles); + } + + function _toUint256Array(uint8[] memory a) internal pure returns (uint256[] memory result) { + assembly ("memory-safe") { + result := a + } + } + + function _sampleRoles() internal returns (uint256[] memory roles) { + unchecked { + uint256 n = _random() & 0xf; + roles = DynamicArrayLib.malloc(n); + for (uint256 i; i != n; ++i) { + roles.set(i, _random() & 0xff); + } + } + } +} diff --git a/test/mocks/MockEnumerableRoles.sol b/test/mocks/MockEnumerableRoles.sol new file mode 100644 index 0000000..913c2b9 --- /dev/null +++ b/test/mocks/MockEnumerableRoles.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "clusters/EnumerableRoles.sol"; + +contract MockEnumerableRoles is EnumerableRoles { + address public owner; + + uint256 public MAX_ROLE; + + function setOwner(address value) public { + owner = value; + } + + function setMaxRole(uint256 value) public { + MAX_ROLE = value; + } + + function thisOwner() public view returns (address) { + return _thisOwner(); + } + + function maxRole() public view returns (uint256) { + return _maxRole(); + } +} diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol new file mode 100644 index 0000000..8bc8165 --- /dev/null +++ b/test/utils/Brutalizer.sol @@ -0,0 +1,885 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. +contract Brutalizer { + /// @dev Multiplier for a mulmod Lehmer psuedorandom number generator. + /// Prime, and a primitive root of `_LPRNG_MODULO`. + uint256 private constant _LPRNG_MULTIPLIER = 0x100000000000000000000000000000051; + + /// @dev Modulo for a mulmod Lehmer psuedorandom number generator. (prime) + uint256 private constant _LPRNG_MODULO = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43; + + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeMemory() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + + let cSize := add(codesize(), iszero(codesize())) + if iszero(lt(cSize, 32)) { cSize := sub(cSize, and(mload(0x02), 0x1f)) } + let start := mod(mload(0x10), cSize) + let size := mul(sub(cSize, start), gt(cSize, start)) + let times := div(0x7ffff, cSize) + if iszero(lt(times, 128)) { times := 128 } + + // Occasionally offset the offset by a pseudorandom large amount. + // Can't be too large, or we will easily get out-of-gas errors. + offset := add(offset, mul(iszero(and(r, 0xf00000000)), and(shr(64, r), 0xfffff))) + + // Fill the free memory with garbage. + // prettier-ignore + for { let w := not(0) } 1 {} { + mstore(offset, mload(0x00)) + mstore(add(offset, 0x20), mload(0x20)) + offset := add(offset, 0x40) + // We use codecopy instead of the identity precompile + // to avoid polluting the `forge test -vvvv` output with tons of junk. + codecopy(offset, start, size) + codecopy(add(offset, size), 0x00, start) + offset := add(offset, cSize) + times := add(times, w) // `sub(times, 1)`. + if iszero(times) { break } + } + // With a 1/16 chance, copy the contract's code to the scratch space. + if iszero(and(0xf00, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the scratch space with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeScratchSpace() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + if iszero(and(0xf00, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the lower memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + /// For efficiency, this only fills a small portion of the free memory. + function _brutalizeLowerMemory() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + + for {} 1 {} { + if iszero(and(0x7000, r)) { + let x := keccak256(zero, 0x40) + mstore(offset, x) + mstore(add(0x20, offset), x) + mstore(add(0x40, offset), x) + mstore(add(0x60, offset), x) + mstore(add(0x80, offset), x) + mstore(add(0xa0, offset), x) + mstore(add(0xc0, offset), x) + mstore(add(0xe0, offset), x) + mstore(add(0x100, offset), x) + mstore(add(0x120, offset), x) + mstore(add(0x140, offset), x) + mstore(add(0x160, offset), x) + mstore(add(0x180, offset), x) + mstore(add(0x1a0, offset), x) + mstore(add(0x1c0, offset), x) + mstore(add(0x1e0, offset), x) + mstore(add(0x200, offset), x) + mstore(add(0x220, offset), x) + mstore(add(0x240, offset), x) + mstore(add(0x260, offset), x) + break + } + codecopy(offset, byte(0, r), codesize()) + break + } + if iszero(and(0x300, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeMemory() { + _brutalizeMemory(); + _; + _checkMemory(); + } + + /// @dev Fills the scratch space with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeScratchSpace() { + _brutalizeScratchSpace(); + _; + _checkMemory(); + } + + /// @dev Fills the lower memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeLowerMemory() { + _brutalizeLowerMemory(); + _; + _checkMemory(); + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalized(address value) internal pure returns (address result) { + uint256 r = uint256(uint160(value)); + r = (__brutalizerRandomness(r) << 160) ^ r; + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint8(uint8 value) internal pure returns (uint8 result) { + uint256 r = (__brutalizerRandomness(value) << 8) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes1(bytes1 value) internal pure returns (bytes1 result) { + bytes32 r = __brutalizedBytesN(value, 8); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint16(uint16 value) internal pure returns (uint16 result) { + uint256 r = (__brutalizerRandomness(value) << 16) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes2(bytes2 value) internal pure returns (bytes2 result) { + bytes32 r = __brutalizedBytesN(value, 16); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint24(uint24 value) internal pure returns (uint24 result) { + uint256 r = (__brutalizerRandomness(value) << 24) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes3(bytes3 value) internal pure returns (bytes3 result) { + bytes32 r = __brutalizedBytesN(value, 24); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint32(uint32 value) internal pure returns (uint32 result) { + uint256 r = (__brutalizerRandomness(value) << 32) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes4(bytes4 value) internal pure returns (bytes4 result) { + bytes32 r = __brutalizedBytesN(value, 32); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint40(uint40 value) internal pure returns (uint40 result) { + uint256 r = (__brutalizerRandomness(value) << 40) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes5(bytes5 value) internal pure returns (bytes5 result) { + bytes32 r = __brutalizedBytesN(value, 40); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint48(uint48 value) internal pure returns (uint48 result) { + uint256 r = (__brutalizerRandomness(value) << 48) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes6(bytes6 value) internal pure returns (bytes6 result) { + bytes32 r = __brutalizedBytesN(value, 48); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint56(uint56 value) internal pure returns (uint56 result) { + uint256 r = (__brutalizerRandomness(value) << 56) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes7(bytes7 value) internal pure returns (bytes7 result) { + bytes32 r = __brutalizedBytesN(value, 56); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint64(uint64 value) internal pure returns (uint64 result) { + uint256 r = (__brutalizerRandomness(value) << 64) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes8(bytes8 value) internal pure returns (bytes8 result) { + bytes32 r = __brutalizedBytesN(value, 64); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint72(uint72 value) internal pure returns (uint72 result) { + uint256 r = (__brutalizerRandomness(value) << 72) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes9(bytes9 value) internal pure returns (bytes9 result) { + bytes32 r = __brutalizedBytesN(value, 72); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint80(uint80 value) internal pure returns (uint80 result) { + uint256 r = (__brutalizerRandomness(value) << 80) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes10(bytes10 value) internal pure returns (bytes10 result) { + bytes32 r = __brutalizedBytesN(value, 80); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint88(uint88 value) internal pure returns (uint88 result) { + uint256 r = (__brutalizerRandomness(value) << 88) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes11(bytes11 value) internal pure returns (bytes11 result) { + bytes32 r = __brutalizedBytesN(value, 88); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint96(uint96 value) internal pure returns (uint96 result) { + uint256 r = (__brutalizerRandomness(value) << 96) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes12(bytes12 value) internal pure returns (bytes12 result) { + bytes32 r = __brutalizedBytesN(value, 96); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint104(uint104 value) internal pure returns (uint104 result) { + uint256 r = (__brutalizerRandomness(value) << 104) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes13(bytes13 value) internal pure returns (bytes13 result) { + bytes32 r = __brutalizedBytesN(value, 104); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint112(uint112 value) internal pure returns (uint112 result) { + uint256 r = (__brutalizerRandomness(value) << 112) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes14(bytes14 value) internal pure returns (bytes14 result) { + bytes32 r = __brutalizedBytesN(value, 112); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint120(uint120 value) internal pure returns (uint120 result) { + uint256 r = (__brutalizerRandomness(value) << 120) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes15(bytes15 value) internal pure returns (bytes15 result) { + bytes32 r = __brutalizedBytesN(value, 120); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint128(uint128 value) internal pure returns (uint128 result) { + uint256 r = (__brutalizerRandomness(value) << 128) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes16(bytes16 value) internal pure returns (bytes16 result) { + bytes32 r = __brutalizedBytesN(value, 128); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint136(uint136 value) internal pure returns (uint136 result) { + uint256 r = (__brutalizerRandomness(value) << 136) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes17(bytes17 value) internal pure returns (bytes17 result) { + bytes32 r = __brutalizedBytesN(value, 136); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint144(uint144 value) internal pure returns (uint144 result) { + uint256 r = (__brutalizerRandomness(value) << 144) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes18(bytes18 value) internal pure returns (bytes18 result) { + bytes32 r = __brutalizedBytesN(value, 144); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint152(uint152 value) internal pure returns (uint152 result) { + uint256 r = (__brutalizerRandomness(value) << 152) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes19(bytes19 value) internal pure returns (bytes19 result) { + bytes32 r = __brutalizedBytesN(value, 152); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint160(uint160 value) internal pure returns (uint160 result) { + uint256 r = (__brutalizerRandomness(value) << 160) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes20(bytes20 value) internal pure returns (bytes20 result) { + bytes32 r = __brutalizedBytesN(value, 160); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint168(uint168 value) internal pure returns (uint168 result) { + uint256 r = (__brutalizerRandomness(value) << 168) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes21(bytes21 value) internal pure returns (bytes21 result) { + bytes32 r = __brutalizedBytesN(value, 168); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint176(uint176 value) internal pure returns (uint176 result) { + uint256 r = (__brutalizerRandomness(value) << 176) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes22(bytes22 value) internal pure returns (bytes22 result) { + bytes32 r = __brutalizedBytesN(value, 176); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint184(uint184 value) internal pure returns (uint184 result) { + uint256 r = (__brutalizerRandomness(value) << 184) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes23(bytes23 value) internal pure returns (bytes23 result) { + bytes32 r = __brutalizedBytesN(value, 184); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint192(uint192 value) internal pure returns (uint192 result) { + uint256 r = (__brutalizerRandomness(value) << 192) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes24(bytes24 value) internal pure returns (bytes24 result) { + bytes32 r = __brutalizedBytesN(value, 192); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint200(uint200 value) internal pure returns (uint200 result) { + uint256 r = (__brutalizerRandomness(value) << 200) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes25(bytes25 value) internal pure returns (bytes25 result) { + bytes32 r = __brutalizedBytesN(value, 200); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint208(uint208 value) internal pure returns (uint208 result) { + uint256 r = (__brutalizerRandomness(value) << 208) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes26(bytes26 value) internal pure returns (bytes26 result) { + bytes32 r = __brutalizedBytesN(value, 208); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint216(uint216 value) internal pure returns (uint216 result) { + uint256 r = (__brutalizerRandomness(value) << 216) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes27(bytes27 value) internal pure returns (bytes27 result) { + bytes32 r = __brutalizedBytesN(value, 216); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint224(uint224 value) internal pure returns (uint224 result) { + uint256 r = (__brutalizerRandomness(value) << 224) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes28(bytes28 value) internal pure returns (bytes28 result) { + bytes32 r = __brutalizedBytesN(value, 224); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint232(uint232 value) internal pure returns (uint232 result) { + uint256 r = (__brutalizerRandomness(value) << 232) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes29(bytes29 value) internal pure returns (bytes29 result) { + bytes32 r = __brutalizedBytesN(value, 232); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint240(uint240 value) internal pure returns (uint240 result) { + uint256 r = (__brutalizerRandomness(value) << 240) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes30(bytes30 value) internal pure returns (bytes30 result) { + bytes32 r = __brutalizedBytesN(value, 240); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint248(uint248 value) internal pure returns (uint248 result) { + uint256 r = (__brutalizerRandomness(value) << 248) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes31(bytes31 value) internal pure returns (bytes31 result) { + bytes32 r = __brutalizedBytesN(value, 248); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalized(bool value) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + calldatacopy(result, 0x00, calldatasize()) + mstore(0x20, keccak256(result, calldatasize())) + mstore(0x10, xor(value, mload(0x10))) + let r := keccak256(0x00, 0x88) + mstore(0x10, r) + result := mul(iszero(iszero(value)), r) + if iszero(and(1, shr(128, mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO)))) { + result := iszero(iszero(result)) + } + } + } + + /// @dev Returns a brutalizer randomness. + function __brutalizedBytesN(bytes32 x, uint256 s) private pure returns (bytes32) { + return bytes32(uint256((__brutalizerRandomness(uint256(x)) >> s) ^ uint256(x))); + } + + /// @dev Returns a brutalizer randomness. + function __brutalizerRandomness(uint256 seed) private pure returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + calldatacopy(result, 0x00, calldatasize()) + mstore(0x20, keccak256(result, calldatasize())) + mstore(0x10, xor(seed, mload(0x10))) + result := keccak256(0x00, 0x88) + mstore(0x10, result) + if iszero(and(7, shr(128, mulmod(result, _LPRNG_MULTIPLIER, _LPRNG_MODULO)))) { result := 0 } + } + } + + /// @dev Misaligns the free memory pointer. + /// The free memory pointer has a 1/32 chance to be aligned. + function _misalignFreeMemoryPointer() internal pure { + uint256 twoWords = 0x40; + /// @solidity memory-safe-assembly + assembly { + let m := mload(twoWords) + m := add(m, mul(and(keccak256(0x00, twoWords), 0x1f), iszero(and(m, 0x1f)))) + mstore(twoWords, m) + } + } + + /// @dev Check if the free memory pointer and the zero slot are not contaminated. + /// Useful for cases where these slots are used for temporary storage. + function _checkMemory() internal pure { + bool zeroSlotIsNotZero; + bool freeMemoryPointerOverflowed; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Test at a lower, but reasonable limit for more safety room. + if gt(mload(0x40), 0xffffffff) { freeMemoryPointerOverflowed := 1 } + // Check the value of the zero slot. + zeroSlotIsNotZero := mload(0x60) + } + if (freeMemoryPointerOverflowed) revert("`0x40` overflowed!"); + if (zeroSlotIsNotZero) revert("`0x60` is not zero!"); + } + + /// @dev Check if `s`: + /// - Has sufficient memory allocated. + /// - Is zero right padded (cuz some frontends like Etherscan has issues + /// with decoding non-zero-right-padded strings). + function _checkMemory(bytes memory s) internal pure { + bool notZeroRightPadded; + bool insufficientMalloc; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + let length := mload(s) + let lastWord := mload(add(add(s, 0x20), and(length, not(0x1f)))) + let remainder := and(length, 0x1f) + if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } + // Check if the memory allocated is sufficient. + if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } + } + if (notZeroRightPadded) revert("Not zero right padded!"); + if (insufficientMalloc) revert("Insufficient memory allocation!"); + _checkMemory(); + } + + /// @dev For checking the memory allocation for string `s`. + function _checkMemory(string memory s) internal pure { + _checkMemory(bytes(s)); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(uint256[] memory a) internal pure { + bool insufficientMalloc; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Check if the memory allocated is sufficient. + insufficientMalloc := gt(add(add(a, 0x20), shl(5, mload(a))), mload(0x40)) + } + if (insufficientMalloc) revert("Insufficient memory allocation!"); + _checkMemory(); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(bytes32[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(address[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(bool[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } +} diff --git a/test/utils/SoladyTest.sol b/test/utils/SoladyTest.sol new file mode 100644 index 0000000..21c29f6 --- /dev/null +++ b/test/utils/SoladyTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "./TestPlus.sol"; + +contract SoladyTest is Test, TestPlus { + /// @dev Alias for `_hem`. + function _bound(uint256 x, uint256 min, uint256 max) internal pure virtual override returns (uint256) { + return _hem(x, min, max); + } +} diff --git a/test/utils/TestPlus.sol b/test/utils/TestPlus.sol new file mode 100644 index 0000000..52ec513 --- /dev/null +++ b/test/utils/TestPlus.sol @@ -0,0 +1,686 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Brutalizer} from "./Brutalizer.sol"; + +contract TestPlus is Brutalizer { + event LogString(string name, string value); + event LogString(string value); + event LogBytes(string name, bytes value); + event LogBytes(bytes value); + event LogUint(string name, uint256 value); + event LogUint(uint256 value); + event LogBytes32(string name, bytes32 value); + event LogBytes32(bytes32 value); + event LogInt(string name, int256 value); + event LogInt(int256 value); + event LogAddress(string name, address value); + event LogAddress(address value); + event LogBool(string name, bool value); + event LogBool(bool value); + + event LogStringArray(string name, string[] value); + event LogStringArray(string[] value); + event LogBytesArray(string name, bytes[] value); + event LogBytesArray(bytes[] value); + event LogUintArray(string name, uint256[] value); + event LogUintArray(uint256[] value); + event LogBytes32Array(string name, bytes32[] value); + event LogBytes32Array(bytes32[] value); + event LogIntArray(string name, int256[] value); + event LogIntArray(int256[] value); + event LogAddressArray(string name, address[] value); + event LogAddressArray(address[] value); + event LogBoolArray(string name, bool[] value); + event LogBoolArray(bool[] value); + + /// @dev `address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))`. + address private constant _VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + + /// @dev This is the keccak256 of a very long string I randomly mashed on my keyboard. + uint256 private constant _TESTPLUS_RANDOMNESS_SLOT = + 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee; + + /// @dev The maximum private key. + uint256 private constant _PRIVATE_KEY_MAX = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140; + + /// @dev Some constant to brutalize the upper bits of addresses. + uint256 private constant _ADDRESS_BRUTALIZER = 0xc0618c2bfd481dcf3e31738f; + + /// @dev Multiplier for a mulmod Lehmer psuedorandom number generator. + /// Prime, and a primitive root of `_LPRNG_MODULO`. + uint256 private constant _LPRNG_MULTIPLIER = 0x100000000000000000000000000000051; + + /// @dev Modulo for a mulmod Lehmer psuedorandom number generator. (prime) + uint256 private constant _LPRNG_MODULO = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43; + + /// @dev Returns whether the `value` has been generated for `typeId` and `groupId` before. + function __markAsGenerated(bytes32 typeId, bytes32 groupId, uint256 value) private returns (bool isSet) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + mstore(0x00, value) + mstore(0x20, groupId) + mstore(0x40, typeId) + mstore(0x60, _TESTPLUS_RANDOMNESS_SLOT) + let s := keccak256(0x00, 0x80) + isSet := sload(s) + sstore(s, 1) + mstore(0x40, m) // Restore the free memory pointer. + mstore(0x60, 0) // Restore the zero pointer. + } + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + /// This function may return a previously returned result. + function _random() internal returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := _TESTPLUS_RANDOMNESS_SLOT + let sValue := sload(result) + mstore(0x20, sValue) + let r := keccak256(0x20, 0x40) + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + sValue := result + calldatacopy(mload(0x40), 0x00, calldatasize()) + r := keccak256(mload(0x40), calldatasize()) + } + sstore(result, add(r, 1)) + + // Do some biased sampling for more robust tests. + // prettier-ignore + for {} 1 {} { + let y := mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO) + // With a 1/256 chance, randomly set `r` to any of 0,1,2,3. + if iszero(byte(19, y)) { + r := and(byte(11, y), 3) + break + } + let d := byte(17, y) + // With a 1/2 chance, set `r` to near a random power of 2. + if iszero(and(2, d)) { + // Set `t` either `not(0)` or `xor(sValue, r)`. + let t := or(xor(sValue, r), sub(0, and(1, d))) + // Set `r` to `t` shifted left or right. + // prettier-ignore + for {} 1 {} { + if iszero(and(8, d)) { + if iszero(and(16, d)) { t := 1 } + if iszero(and(32, d)) { + r := add(shl(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) + break + } + r := add(shl(byte(7, y), t), sub(511, and(1023, r))) + break + } + if iszero(and(16, d)) { t := shl(255, 1) } + if iszero(and(32, d)) { + r := add(shr(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) + break + } + r := add(shr(byte(7, y), t), sub(511, and(1023, r))) + break + } + // With a 1/2 chance, negate `r`. + r := xor(sub(0, shr(7, d)), r) + break + } + // Otherwise, just set `r` to `xor(sValue, r)`. + r := xor(sValue, r) + break + } + result := r + } + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique(uint256 groupId) internal returns (uint256 result) { + result = _randomUnique(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique(bytes32 groupId) internal returns (uint256 result) { + do { + result = _random(); + } while (__markAsGenerated("uint256", groupId, result)); + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique() internal returns (uint256 result) { + result = _randomUnique(""); + } + + /// @dev Returns a pseudorandom number, uniformly distributed in [0 .. 2**256 - 1] (inclusive). + function _randomUniform() internal returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := _TESTPLUS_RANDOMNESS_SLOT + // prettier-ignore + for { let sValue := sload(result) } 1 {} { + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + calldatacopy(mload(0x40), 0x00, calldatasize()) + sValue := keccak256(mload(0x40), calldatasize()) + sstore(result, sValue) + result := sValue + break + } + mstore(0x1f, sValue) + sValue := keccak256(0x20, 0x40) + sstore(result, sValue) + result := sValue + break + } + } + } + + /// @dev Returns a boolean with an approximately 1/n chance of being true. + /// This function may return a previously returned result. + function _randomChance(uint256 n) internal returns (bool result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := iszero(mod(r, n)) + } + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + /// This function may return a previously returned result. + function _randomPrivateKey() internal returns (uint256 result) { + result = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + for {} 1 {} { + if iszero(and(result, 0x10)) { + if iszero(and(result, 0x20)) { + result := add(and(result, 0xf), 1) + break + } + result := sub(_PRIVATE_KEY_MAX, and(result, 0xf)) + break + } + result := shr(1, result) + break + } + } + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey(uint256 groupId) internal returns (uint256 result) { + result = _randomUniquePrivateKey(bytes32(groupId)); + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey(bytes32 groupId) internal returns (uint256 result) { + do { + result = _randomPrivateKey(); + } while (__markAsGenerated("uint256", groupId, result)); + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey() internal returns (uint256 result) { + result = _randomUniquePrivateKey(""); + } + + /// @dev Private helper function to get the signer from a private key. + function __getSigner(uint256 privateKey) private view returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0xffa18649) // `addr(uint256)`. + mstore(0x20, privateKey) + result := mload(staticcall(gas(), _VM_ADDRESS, 0x1c, 0x24, 0x01, 0x20)) + } + } + + /// @dev Private helper to ensure an address is brutalized. + function __toBrutalizedAddress(address a) private pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := keccak256(0x00, 0x88) + result := xor(shl(160, xor(result, _ADDRESS_BRUTALIZER)), a) + mstore(0x10, result) + } + } + + /// @dev Private helper to ensure an address is brutalized. + function __toBrutalizedAddress(uint256 a) private pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := keccak256(0x00, 0x88) + result := xor(shl(160, xor(result, _ADDRESS_BRUTALIZER)), a) + mstore(0x10, result) + } + } + + /// @dev Returns a pseudorandom signer and its private key. + /// This function may return a previously returned result. + /// The signer may have dirty upper 96 bits. + function _randomSigner() internal returns (address signer, uint256 privateKey) { + privateKey = _randomPrivateKey(); + signer = __toBrutalizedAddress(__getSigner(privateKey)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner(uint256 groupId) internal returns (address signer, uint256 privateKey) { + (signer, privateKey) = _randomUniqueSigner(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner(bytes32 groupId) internal returns (address signer, uint256 privateKey) { + privateKey = _randomUniquePrivateKey(groupId); + signer = __toBrutalizedAddress(__getSigner(privateKey)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner() internal returns (address signer, uint256 privateKey) { + (signer, privateKey) = _randomUniqueSigner(""); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function may return a previously returned result. + function _randomAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := xor(shl(158, r), and(sub(7, shr(252, r)), r)) + } + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress() internal returns (address result) { + result = _randomUniqueAddress(""); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function may return a previously returned result. + function _randomNonZeroAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := xor(shl(158, r), and(sub(7, shr(252, r)), r)) + if iszero(shl(96, result)) { + mstore(0x00, result) + result := keccak256(0x00, 0x30) + } + } + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueNonZeroAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomNonZeroAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress() internal returns (address result) { + result = _randomUniqueNonZeroAddress(""); + } + + /// @dev Cleans the upper 96 bits of the address. + /// This is included so that CI passes for older solc versions with --via-ir. + function _cleaned(address a) internal pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := shr(96, shl(96, a)) + } + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function may return a previously returned result. + function _randomAddressWithVmVars() internal returns (address result) { + if (_randomChance(8)) result = __toBrutalizedAddress(_randomVmVar()); + else result = _randomAddress(); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function may return a previously returned result. + function _randomNonZeroAddressWithVmVars() internal returns (address result) { + do { + if (_randomChance(8)) result = __toBrutalizedAddress(_randomVmVar()); + else result = _randomAddress(); + } while (result == address(0)); + } + + /// @dev Returns a random variable in the virtual machine. + function _randomVmVar() internal returns (uint256 result) { + uint256 r = _randomUniform(); + uint256 t = r % 11; + if (t <= 4) { + if (t == 0) return uint160(address(this)); + if (t == 1) return uint160(tx.origin); + if (t == 2) return uint160(msg.sender); + if (t == 3) return uint160(_VM_ADDRESS); + if (t == 4) return uint160(0x000000000000000000636F6e736F6c652e6c6f67); + } + uint256 y = r >> 32; + if (t == 5) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, r) + codecopy(0x00, mod(and(y, 0xffff), add(codesize(), 0x20)), 0x20) + result := mload(0x00) + } + return result; + } + if (t == 6) { + /// @solidity memory-safe-assembly + assembly { + calldatacopy(0x00, mod(and(y, 0xffff), add(calldatasize(), 0x20)), 0x20) + result := mload(0x00) + } + return result; + } + if (t == 7) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + returndatacopy(m, 0x00, returndatasize()) + result := mload(add(m, mod(and(y, 0xffff), add(returndatasize(), 0x20)))) + } + return result; + } + if (t == 8) { + /// @solidity memory-safe-assembly + assembly { + result := sload(and(y, 0xff)) + } + return result; + } + if (t == 9) { + /// @solidity memory-safe-assembly + assembly { + result := mload(mod(y, add(mload(0x40), 0x40))) + } + return result; + } + result = __getSigner(_randomPrivateKey()); + } + + /// @dev Returns a pseudorandom hashed address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function will not return a precompile address. + /// This function will not return a zero address. + /// This function may return a previously returned result. + function _randomHashedAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + mstore(0x1f, and(sub(7, shr(252, r)), r)) + calldatacopy(0x00, 0x00, 0x24) + result := keccak256(0x00, 0x3f) + } + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueHashedAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomHashedAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress() internal returns (address result) { + result = _randomUniqueHashedAddress(""); + } + + /// @dev Private helper function for returning random bytes. + function __randomBytes(bool zeroRightPad) private returns (bytes memory result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + let n := and(r, 0x1ffff) + let t := shr(24, r) + for {} 1 {} { + // With a 1/256 chance, just return the zero pointer as the result. + if iszero(and(t, 0xff0)) { + result := 0x60 + break + } + result := mload(0x40) + // With a 15/16 chance, set the length to be + // exponentially distributed in the range [0,255] (inclusive). + if shr(252, r) { n := shr(and(t, 0x7), byte(5, r)) } + // Store some fixed word at the start of the string. + // We want this function to sometimes return duplicates. + mstore(add(result, 0x20), xor(calldataload(0x00), _TESTPLUS_RANDOMNESS_SLOT)) + // With a 1/2 chance, copy the contract code to the start and end. + if iszero(and(t, 0x1000)) { + // Copy to the start. + if iszero(and(t, 0x2000)) { codecopy(result, byte(1, r), codesize()) } + // Copy to the end. + codecopy(add(result, n), byte(2, r), 0x40) + } + // With a 1/16 chance, randomize the start and end. + if iszero(and(t, 0xf0000)) { + let y := mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO) + mstore(add(result, 0x20), y) + mstore(add(result, n), xor(r, y)) + } + // With a 1/256 chance, make the result entirely zero bytes. + if iszero(byte(4, r)) { codecopy(result, codesize(), add(n, 0x20)) } + // Skip the zero-right-padding if not required. + if iszero(zeroRightPad) { + mstore(0x40, add(n, add(0x40, result))) // Allocate memory. + mstore(result, n) // Store the length. + break + } + mstore(add(add(result, 0x20), n), 0) // Zeroize the word after the result. + mstore(0x40, add(n, add(0x60, result))) // Allocate memory. + mstore(result, n) // Store the length. + break + } + } + } + + /// @dev Returns a random bytes string from 0 to 131071 bytes long. + /// This random bytes string may NOT be zero-right-padded. + /// This is intentional for memory robustness testing. + /// This function may return a previously returned result. + function _randomBytes() internal returns (bytes memory result) { + result = __randomBytes(false); + } + + /// @dev Returns a random bytes string from 0 to 131071 bytes long. + /// This function may return a previously returned result. + function _randomBytesZeroRightPadded() internal returns (bytes memory result) { + result = __randomBytes(true); + } + + /// @dev Truncate the bytes to `n` bytes. + /// Returns the result for function chaining. + function _truncateBytes(bytes memory b, uint256 n) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + if gt(mload(b), n) { mstore(b, n) } + result := b + } + } + + /// @dev Returns the free memory pointer. + function _freeMemoryPointer() internal pure returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + } + } + + /// @dev Increments the free memory pointer by a world. + function _incrementFreeMemoryPointer() internal pure { + uint256 word = 0x20; + /// @solidity memory-safe-assembly + assembly { + mstore(0x40, add(mload(0x40), word)) + } + } + + /// @dev Adapted from `bound`: + /// https://github.com/foundry-rs/forge-std/blob/ff4bf7db008d096ea5a657f2c20516182252a3ed/src/StdUtils.sol#L10 + /// Differentially fuzzed tested against the original implementation. + function _hem(uint256 x, uint256 min, uint256 max) internal pure virtual returns (uint256 result) { + require(min <= max, "Max is less than min."); + /// @solidity memory-safe-assembly + assembly { + // prettier-ignore + for {} 1 {} { + // If `x` is between `min` and `max`, return `x` directly. + // This is to ensure that dictionary values + // do not get shifted if the min is nonzero. + // More info: https://github.com/foundry-rs/forge-std/issues/188 + if iszero(or(lt(x, min), gt(x, max))) { + result := x + break + } + let size := add(sub(max, min), 1) + if lt(gt(x, 3), gt(size, x)) { + result := add(min, x) + break + } + if lt(lt(x, not(3)), gt(size, not(x))) { + result := sub(max, not(x)) + break + } + // Otherwise, wrap x into the range [min, max], + // i.e. the range is inclusive. + if iszero(lt(x, max)) { + let d := sub(x, max) + let r := mod(d, size) + if iszero(r) { + result := max + break + } + result := sub(add(min, r), 1) + break + } + let d := sub(min, x) + let r := mod(d, size) + if iszero(r) { + result := min + break + } + result := add(sub(max, r), 1) + break + } + } + } + + /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. + function _safeCreate2(uint256 payableAmount, bytes32 salt, bytes memory initializationCode) + internal + returns (address deploymentAddress) + { + // Canonical address of 0age's immutable create 2 factory. + address c2f = 0x0000000000FFe8B47B3e2130213B802212439497; + uint256 c2fCodeLength; + /// @solidity memory-safe-assembly + assembly { + c2fCodeLength := extcodesize(c2f) + } + if (c2fCodeLength == 0) { + bytes memory ic2fBytecode = + hex"60806040526004361061003f5760003560e01c806308508b8f1461004457806364e030871461009857806385cf97ab14610138578063a49a7c90146101bc575b600080fd5b34801561005057600080fd5b506100846004803603602081101561006757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101ec565b604080519115158252519081900360200190f35b61010f600480360360408110156100ae57600080fd5b813591908101906040810160208201356401000000008111156100d057600080fd5b8201836020820111156100e257600080fd5b8035906020019184600183028401116401000000008311171561010457600080fd5b509092509050610217565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561014457600080fd5b5061010f6004803603604081101561015b57600080fd5b8135919081019060408101602082013564010000000081111561017d57600080fd5b82018360208201111561018f57600080fd5b803590602001918460018302840111640100000000831117156101b157600080fd5b509092509050610592565b3480156101c857600080fd5b5061010f600480360360408110156101df57600080fd5b508035906020013561069e565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205460ff1690565b600083606081901c33148061024c57507fffffffffffffffffffffffffffffffffffffffff0000000000000000000000008116155b6102a1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260458152602001806107746045913960600191505060405180910390fd5b606084848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920182905250604051855195965090943094508b93508692506020918201918291908401908083835b6020831061033557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016102f8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff018019909216911617905260408051929094018281037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00183528085528251928201929092207fff000000000000000000000000000000000000000000000000000000000000008383015260609890981b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016602183015260358201969096526055808201979097528251808203909701875260750182525084519484019490942073ffffffffffffffffffffffffffffffffffffffff81166000908152938490529390922054929350505060ff16156104a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603f815260200180610735603f913960400191505060405180910390fd5b81602001825188818334f5955050508073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff161461053a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260468152602001806107b96046913960600191505060405180910390fd5b50505073ffffffffffffffffffffffffffffffffffffffff8116600090815260208190526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790559392505050565b6000308484846040516020018083838082843760408051919093018181037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001825280845281516020928301207fff000000000000000000000000000000000000000000000000000000000000008383015260609990991b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000166021820152603581019790975260558088019890985282518088039098018852607590960182525085519585019590952073ffffffffffffffffffffffffffffffffffffffff81166000908152948590529490932054939450505060ff909116159050610697575060005b9392505050565b604080517fff000000000000000000000000000000000000000000000000000000000000006020808301919091523060601b6021830152603582018590526055808301859052835180840390910181526075909201835281519181019190912073ffffffffffffffffffffffffffffffffffffffff81166000908152918290529190205460ff161561072e575060005b9291505056fe496e76616c696420636f6e7472616374206372656174696f6e202d20636f6e74726163742068617320616c7265616479206265656e206465706c6f7965642e496e76616c69642073616c74202d206669727374203230206279746573206f66207468652073616c74206d757374206d617463682063616c6c696e6720616464726573732e4661696c656420746f206465706c6f7920636f6e7472616374207573696e672070726f76696465642073616c7420616e6420696e697469616c697a6174696f6e20636f64652ea265627a7a723058202bdc55310d97c4088f18acf04253db593f0914059f0c781a9df3624dcef0d1cf64736f6c634300050a0032"; + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + mstore(m, 0xb4d6c782) // `etch(address,bytes)`. + mstore(add(m, 0x20), c2f) + mstore(add(m, 0x40), 0x40) + let n := mload(ic2fBytecode) + mstore(add(m, 0x60), n) + for { let i := 0 } lt(i, n) { i := add(0x20, i) } { + mstore(add(add(m, 0x80), i), mload(add(add(ic2fBytecode, 0x20), i))) + } + pop(call(gas(), _VM_ADDRESS, 0, add(m, 0x1c), add(n, 0x64), 0x00, 0x00)) + } + } + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + let n := mload(initializationCode) + mstore(m, 0x64e03087) // `safeCreate2(bytes32,bytes)`. + mstore(add(m, 0x20), salt) + mstore(add(m, 0x40), 0x40) + mstore(add(m, 0x60), n) + // prettier-ignore + for { let i := 0 } lt(i, n) { i := add(i, 0x20) } { + mstore(add(add(m, 0x80), i), mload(add(add(initializationCode, 0x20), i))) + } + if iszero(call(gas(), c2f, payableAmount, add(m, 0x1c), add(n, 0x64), m, 0x20)) { + returndatacopy(m, m, returndatasize()) + revert(m, returndatasize()) + } + deploymentAddress := mload(m) + } + } + + /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. + function _safeCreate2(bytes32 salt, bytes memory initializationCode) internal returns (address deploymentAddress) { + deploymentAddress = _safeCreate2(0, salt, initializationCode); + } + + /// @dev This function will make forge's gas output display the approximate codesize of + /// the test contract as the amount of gas burnt. Useful for quick guess checking if + /// certain optimizations actually compiles to similar bytecode. + function test__codesize() external view { + /// @solidity memory-safe-assembly + assembly { + // If the caller is the contract itself (i.e. recursive call), burn all the gas. + if eq(caller(), address()) { invalid() } + mstore(0x00, 0xf09ff470) // Store the function selector of `test__codesize()`. + pop(staticcall(codesize(), address(), 0x1c, 0x04, 0x00, 0x00)) + } + } +} From 02a7066bcf5742f394a6505a0c86c98824deb9fb Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 10 Oct 2024 09:59:56 +0000 Subject: [PATCH 02/13] Add create2 build script --- .gitignore | 6 +- build_create2_deployments.sh | 56 ++++++ src/EnumerableRoles.sol | 15 +- src/beta/ClustersCommunityBaseBeta.sol | 81 ++++++++ src/beta/ClustersCommunityHubBeta.sol | 160 +++++++++++++++ src/beta/ClustersCommunityInitiatorBeta.sol | 204 ++++++++++++++++++++ 6 files changed, 518 insertions(+), 4 deletions(-) create mode 100755 build_create2_deployments.sh create mode 100644 src/beta/ClustersCommunityBaseBeta.sol create mode 100644 src/beta/ClustersCommunityHubBeta.sol create mode 100644 src/beta/ClustersCommunityInitiatorBeta.sol diff --git a/.gitignore b/.gitignore index 9e7abee..edb9b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ docs/ # Dotenv file .env -.DS_Store \ No newline at end of file +.DS_Store + +# Create2 build files +.tmp +create2 diff --git a/build_create2_deployments.sh b/build_create2_deployments.sh new file mode 100755 index 0000000..c303e89 --- /dev/null +++ b/build_create2_deployments.sh @@ -0,0 +1,56 @@ +# Run the formatter. +forge fmt; + +# Create the create2 deployments directory. +mkdir create2 > /dev/null 2>&1; + +# Build the Solidity files. +forge build --out="out" --root="."; + +# Use a temporary directory. +mkdir .tmp > /dev/null 2>&1; +rm -r .tmp/out > /dev/null 2>&1; +cp -r out .tmp/out > /dev/null 2>&1; + +# Go into the temporary directory. +cd .tmp; + +# Install some files for computing the initcodehash. +echo '{ "devDependencies": { "@ethersproject/keccak256": "5.7.0" } }' > package.json; +if [ ! -f package-lock.json ]; then npm install; fi + +# Create the deployments directory in the temporary directory. +mkdir create2 > /dev/null 2>&1; + +# Function to generate the deployment files. +generateDeployment() { + rm -rf "create2/$1" > /dev/null 2>&1; + mkdir "create2/$1" > /dev/null 2>&1; + # Generate the js file to do the hard work. + echo " + const fs = require(\"fs\"), + rfs = s => fs.readFileSync(s, { encoding: \"utf8\", flag: \"r\" }); + const solcOutput = JSON.parse(rfs(\"out/$1.sol/$1.json\")); + const initcode = solcOutput[\"bytecode\"][\"object\"].slice(2); + const d = \"create2/$1\"; + fs.writeFileSync(d + \"/initcode.txt\", initcode); + const t = solcOutput[\"metadata\"][\"settings\"][\"compilationTarget\"], k = Object.keys(t)[0]; + fs.writeFileSync(d + \"/t\", k + \":\" + t[k]); + fs.writeFileSync(d + \"/initcodehash.txt\", require(\"@ethersproject/keccak256\").keccak256(\"0x\" + initcode)); + " > "extract_$1.js"; + # Run the js file. + node "extract_$1.js"; + # Generate the standard json verification file. + forge verify-contract $(cast --address-zero) "$( "create2/$1/input.json"; + # Remove the temporary files. + rm "create2/$1/t" > /dev/null 2>&1; + rm "extract_$1.js" > /dev/null 2>&1; + # Move the directory over to the actual deployment directory. + rm -rf "../create2/$1" > /dev/null 2>&1; + cp -r "create2/$1" "../create2/$1"; + rm -rf "create2/$1" > /dev/null 2>&1; +} + +# Generate the deployments. +generateDeployment "ClustersCommunityHubBeta"; +generateDeployment "ClustersCommunityInitiatorBeta"; diff --git a/src/EnumerableRoles.sol b/src/EnumerableRoles.sol index 2de408b..9ab5a1a 100644 --- a/src/EnumerableRoles.sol +++ b/src/EnumerableRoles.sol @@ -106,11 +106,20 @@ contract EnumerableRoles { _; } + /// @dev Guards a function such that the caller must be the contact owner or have `role0` or `role1`. + modifier onlyOwnerOrAnyRole(uint8 role0, uint8 role1) virtual { + _checkOwnerOrAnyRole(role0, role1); + _; + } + + /// @dev Requires that the caller is the contract owner or has `role`. + function _checkOwnerOrAnyRole(uint8 role0, uint8 role1) internal virtual { + if (msg.sender != _thisOwner() && !hasRole(msg.sender, role0)) _checkRole(role1); + } + /// @dev Requires that the caller is the contract owner or has `role`. function _checkOwnerOrRole(uint8 role) internal virtual { - if (msg.sender != _thisOwner()) { - if (!hasRole(msg.sender, role)) revert CheckRoleUnauthorized(); - } + if (msg.sender != _thisOwner()) _checkRole(role); } /// @dev Requires that the caller has `role`. diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol new file mode 100644 index 0000000..5b62cb3 --- /dev/null +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {EnumerableRoles} from "clusters/EnumerableRoles.sol"; + +contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONSTANTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Admin role. + uint8 public constant ADMIN_ROLE = 0; + + /// @dev Withdrawer role. + uint8 public constant WITHDRAWER_ROLE = 1; + + /// @dev Max role. + uint8 public constant MAX_ROLE = 1; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* WITHDRAW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Allows the owner to withdraw ERC20 tokens. + function withdrawERC20(address token, address to, uint256 amount) + public + onlyOwnerOrAnyRole(ADMIN_ROLE, WITHDRAWER_ROLE) + { + SafeTransferLib.safeTransfer(token, to, amount); + } + + /// @dev Allows the owner to withdraw native currency. + function withdrawNative(address to, uint256 amount) public onlyOwnerOrAnyRole(ADMIN_ROLE, WITHDRAWER_ROLE) { + SafeTransferLib.safeTransferETH(to, amount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns `a[i]`, without bounds checking. + function _get(address[] calldata a, uint256 i) internal pure returns (address result) { + assembly ("memory-safe") { + result := calldataload(add(a.offset, shl(5, i))) + } + } + + /// @dev Returns `a[i]`, without bounds checking. + function _get(uint256[] calldata a, uint256 i) internal pure returns (uint256 result) { + assembly ("memory-safe") { + result := calldataload(add(a.offset, shl(5, i))) + } + } + + /// @dev Returns `a[i]`, without bounds checking. + function _get(bytes32[] calldata a, uint256 i) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + result := calldataload(add(a.offset, shl(5, i))) + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Allow admins to set roles too. + function setRole(address user, uint8 role, bool active) + public + payable + virtual + override + onlyOwnerOrRole(ADMIN_ROLE) + { + _setRole(user, role, active); + } + + /// @dev For UUPSUpgradeable. Only the owner can upgrade. + function _authorizeUpgrade(address newImplementation) internal override onlyOwnerOrRole(ADMIN_ROLE) {} +} diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol new file mode 100644 index 0000000..abbb49d --- /dev/null +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ReentrancyGuard} from "soledge/utils/ReentrancyGuard.sol"; +import {ClustersCommunityBaseBeta} from "clusters/beta/ClustersCommunityBaseBeta.sol"; +import {Origin, OAppReceiverUpgradeable} from "layerzero-oapp/contracts/oapp-upgradeable/OAppReceiverUpgradeable.sol"; + +contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, ClustersCommunityBaseBeta { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* TRANSIENT STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Transient storage slot to denote if the context is in the middle of a `_lzReceive`. + uint256 internal constant _IN_LZ_RECEIVE_TRANSIENT_SLOT = 0; + + /// @dev Transient storage slot for the sender. + uint256 internal constant _SENDER_TRANSIENT_SLOT = 1; + + /// @dev Transient storage slot for the message's origin chain ID. + uint256 internal constant _CHAIN_ID_TRANSIENT_SLOT = 2; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Emitted when a bid is received. + event Bid( + bytes32 from, uint256 tokenChainId, address token, uint256 amount, bytes32 communityName, bytes32 walletName + ); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Insufficient native payment. + error InsufficientNativePayment(); + + /// @dev The input arrays must have the same length. + error ArrayLengthsMismatch(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract. + function initialize(address endpoint_, address owner_) public initializer onlyProxy { + _initializeOAppCore(endpoint_, owner_); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC WRITE FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Places a bid. + /// If any token is `address(0)`, it is treated as the native token. + function placeBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName) + public + payable + nonReentrant + { + (bytes32 sender, uint256 chainId) = _senderAndChainId(); + if (chainId == block.chainid) { + if (token == address(0)) { + if (msg.value < amount) revert InsufficientNativePayment(); + } else { + SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + } + } + emit Bid(sender, chainId, token, amount, communityName, walletName); + } + + /// @dev Places multiple bids. + /// If any token is `address(0)`, it is treated as the native token. + function placeBids( + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[] calldata communityNames, + bytes32[] calldata walletNames + ) public payable nonReentrant { + (bytes32 sender, uint256 chainId) = _senderAndChainId(); + uint256 requiredNativeValue; + if (tokens.length != amounts.length) revert ArrayLengthsMismatch(); + if (tokens.length != walletNames.length) revert ArrayLengthsMismatch(); + if (tokens.length != communityNames.length) revert ArrayLengthsMismatch(); + for (uint256 i; i < tokens.length; ++i) { + address token = _get(tokens, i); + uint256 amount = _get(amounts, i); + if (chainId == block.chainid) { + if (token == address(0)) { + requiredNativeValue += amount; + } else { + SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + } + } + emit Bid(sender, chainId, token, amount, _get(communityNames, i), _get(walletNames, i)); + } + if (msg.value < requiredNativeValue) revert InsufficientNativePayment(); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* LAYERZERO */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev For receiving a bid. + function _lzReceive( + Origin calldata, /* origin */ + bytes32, /* guid */ + bytes calldata message, + address, /* executor */ + bytes calldata /* extraData */ + ) internal override { + assembly ("memory-safe") { + let chainId := calldataload(add(message.offset, 0x00)) + let sender := calldataload(add(message.offset, 0x20)) + + let o := add(message.offset, calldataload(add(message.offset, 0x40))) + let dataLength := calldataload(o) + let dataOffset := add(o, 0x20) + // Check that all of the data is within bounds. + if or(lt(message.length, 0x60), gt(add(dataOffset, dataLength), add(message.offset, message.length))) { + invalid() + } + + tstore(_IN_LZ_RECEIVE_TRANSIENT_SLOT, address()) + tstore(_CHAIN_ID_TRANSIENT_SLOT, chainId) + tstore(_SENDER_TRANSIENT_SLOT, sender) + + let m := mload(0x40) + calldatacopy(m, dataOffset, dataLength) + // Self-delegatecall with `data`. + if iszero(delegatecall(gas(), address(), m, dataLength, 0x00, 0x00)) { + // Bubble up the revert if the self-delegatecall fails. + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + + tstore(_SENDER_TRANSIENT_SLOT, 0) + tstore(_CHAIN_ID_TRANSIENT_SLOT, 0) + tstore(_IN_LZ_RECEIVE_TRANSIENT_SLOT, 0) + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the sender and the chain ID, + /// using the values passed in via Layerzero if they are provided. + function _senderAndChainId() internal view returns (bytes32 sender, uint256 chainId) { + assembly ("memory-safe") { + sender := caller() + chainId := chainid() + if tload(_IN_LZ_RECEIVE_TRANSIENT_SLOT) { + sender := tload(_SENDER_TRANSIENT_SLOT) + chainId := tload(_CHAIN_ID_TRANSIENT_SLOT) + } + } + } +} diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol new file mode 100644 index 0000000..d75f5e8 --- /dev/null +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; +import {ClustersCommunityBaseBeta} from "clusters/beta/ClustersCommunityBaseBeta.sol"; +import {OptionsBuilder} from "layerzero-oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import { + OAppSenderUpgradeable, MessagingFee +} from "layerzero-oapp/contracts/oapp-upgradeable/OAppSenderUpgradeable.sol"; + +contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuard, ClustersCommunityBaseBeta { + using OptionsBuilder for bytes; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The storage struct for the contract. + struct ClustersCommunityInitiatorBetaStorage { + uint32 dstEid; + } + + /// @dev Returns the storage struct for the contract. + function _getClustersCommunityInitiatorBetaStorage() + internal + pure + returns (ClustersCommunityInitiatorBetaStorage storage $) + { + assembly ("memory-safe") { + // `uint72(bytes9(keccak256("Clusters.ClustersCommunityInitiatorBetaStorage")))`. + $.slot := 0xc3d7966f8edf259843 // Truncate to 9 bytes to reduce bytecode size. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Emitted when the destination endpoint ID is set. + event DstEidSet(uint32 eid); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Insufficient native payment. + error InsufficientNativePayment(); + + /// @dev The input arrays must have the same length. + error ArrayLengthsMismatch(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract. + function initialize(address endpoint_, address owner_) public initializer onlyProxy { + _initializeOAppCore(endpoint_, owner_); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC WRITE FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Places a bid. + /// If any token is `address(0)`, it is treated as the native token. + /// All tokens will not be bridged. + function placeBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName, uint256 gas) + public + payable + nonReentrant + { + uint256 requiredNativeValue; + if (token == address(0)) { + requiredNativeValue = amount; + } else { + SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + } + _sendBids(_encodeBidMessage(token, amount, communityName, walletName), requiredNativeValue, gas); + } + + /// @dev Places multiple bids. + /// If any token is `address(0)`, it is treated as the native token. + /// All tokens will not be bridged. + function placeBids( + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[] calldata communityNames, + bytes32[] calldata walletNames, + uint256 gas + ) public payable nonReentrant { + if (tokens.length != amounts.length) revert ArrayLengthsMismatch(); + if (tokens.length != walletNames.length) revert ArrayLengthsMismatch(); + if (tokens.length != communityNames.length) revert ArrayLengthsMismatch(); + uint256 requiredNativeValue; + for (uint256 i; i < tokens.length; ++i) { + address token = _get(tokens, i); + uint256 amount = _get(amounts, i); + if (token == address(0)) { + requiredNativeValue += amount; + } else { + SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + } + } + _sendBids(_encodeBidsMessage(tokens, amounts, communityNames, walletNames), requiredNativeValue, gas); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the Layerzero destination endpoint ID. + function dstEid() public view returns (uint32) { + return _getClustersCommunityInitiatorBetaStorage().dstEid; + } + + /// @dev Returns the amount of native gas fee required to place a bid. + function quoteForBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName, uint256 gas) + public + view + returns (uint256) + { + return _quoteNativeFee(_encodeBidMessage(token, amount, communityName, walletName), gas); + } + + /// @dev Returns the amount of native gas fee required to place the bids. + function quoteForBids( + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[] calldata communityNames, + bytes32[] calldata walletNames, + uint256 gas + ) public view returns (uint256) { + return _quoteNativeFee(_encodeBidsMessage(tokens, amounts, communityNames, walletNames), gas); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Enables the owner to set the destination endpoint ID. + function setDstEid(uint32 eid) public onlyOwnerOrRole(ADMIN_ROLE) { + _getClustersCommunityInitiatorBetaStorage().dstEid = eid; + emit DstEidSet(eid); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Used by `placeBid` and `placeBids`. + function _sendBids(bytes memory encoded, uint256 requiredNativeValue, uint256 gas) internal { + uint256 nativeFee = _quoteNativeFee(encoded, gas); + uint256 requiredNativeTotal = nativeFee + requiredNativeValue; + if (msg.value < requiredNativeTotal) revert InsufficientNativePayment(); + _lzSend(dstEid(), encoded, _defaultOptions(gas), MessagingFee(nativeFee, 0), payable(msg.sender)); + if (msg.value > requiredNativeTotal) { + SafeTransferLib.forceSafeTransferETH(msg.sender, msg.value - requiredNativeTotal); + } + } + + /// @dev Returns the default options that encodes `gas`. + function _defaultOptions(uint256 gas) internal pure returns (bytes memory) { + if (gas >= 1 << 128) revert(); + return OptionsBuilder.newOptions().addExecutorLzReceiveOption(uint128(gas), 0); + } + + /// @dev Returns the required native fee for Layerzero. + function _quoteNativeFee(bytes memory encoded, uint256 gas) internal view returns (uint256) { + return _quote(dstEid(), encoded, _defaultOptions(gas), false).nativeFee; + } + + /// @dev Encodes the bid calldata. + function _encodeBidMessage(address token, uint256 amount, bytes32 communityName, bytes32 walletName) + internal + view + returns (bytes memory) + { + return abi.encode( + block.chainid, + msg.sender, + abi.encodeWithSignature( + "placeBid(address,uint256,bytes32,bytes32)", token, amount, communityName, walletName + ) + ); + } + + /// @dev Encodes the bids calldata. + function _encodeBidsMessage( + address[] calldata tokens, + uint256[] calldata amounts, + bytes32[] calldata communityNames, + bytes32[] calldata walletNames + ) internal view returns (bytes memory) { + return abi.encode( + block.chainid, + msg.sender, + abi.encodeWithSignature( + "placeBids(address[],uint256[],bytes32[],bytes32[])", tokens, amounts, communityNames, walletNames + ) + ); + } +} From 3cd69bcdb1572cd087b5333160584e8781f3d3f9 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sat, 12 Oct 2024 12:09:05 +0000 Subject: [PATCH 03/13] T --- .gitmodules | 6 +- lib/solady | 2 +- src/EnumerableRoles.sol | 162 -------------------- src/beta/ClustersCommunityBaseBeta.sol | 27 ++-- src/beta/ClustersCommunityInitiatorBeta.sol | 2 +- test/EnumerableRoles.t.sol | 116 -------------- test/mocks/MockEnumerableRoles.sol | 26 ---- 7 files changed, 16 insertions(+), 325 deletions(-) delete mode 100644 src/EnumerableRoles.sol delete mode 100644 test/EnumerableRoles.t.sol delete mode 100644 test/mocks/MockEnumerableRoles.sol diff --git a/.gitmodules b/.gitmodules index 17aacf0..22e283c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,9 +17,9 @@ [submodule "lib/multicaller"] path = lib/multicaller url = https://github.com/vectorized/multicaller -[submodule "lib/solady"] - path = lib/solady - url = https://github.com/vectorized/solady [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/lib/solady b/lib/solady index efd064c..b81b8e6 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit efd064c9cd8f75ee0eff5b31898642581bbc97fd +Subproject commit b81b8e62d2a4f34a16578df5a14b270ca5b47cf6 diff --git a/src/EnumerableRoles.sol b/src/EnumerableRoles.sol deleted file mode 100644 index 9ab5a1a..0000000 --- a/src/EnumerableRoles.sol +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol"; - -/// @title EnumerableRoles -/// @notice Enumerable roles mixin that does not require inheritance from any specific ownable. -contract EnumerableRoles { - using EnumerableSetLib for *; - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* STORAGE */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev The storage struct for the contract. - struct EnumerableRolesStorage { - // Mapping of `role` to a set of addresses with the role. - mapping(uint8 => EnumerableSetLib.AddressSet) holders; - // Mapping of an address to a set of roles it has. - mapping(address => EnumerableSetLib.Uint8Set) roles; - } - - /// @dev Returns the storage struct for the contract. - function _getEnumerableRolesStorage() internal pure returns (EnumerableRolesStorage storage $) { - assembly ("memory-safe") { - // `uint72(bytes9(keccak256("Clusters.EnumerableRolesStorage")))`. - $.slot := 0x214b1bb45059b2b86 // Truncate to 9 bytes to reduce bytecode size. - } - } - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* EVENTS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev The status of `role` for `user` has been set to `active`. - event RoleSet(address user, uint8 role, bool active); - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* ERRORS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev Not authorized to set the role. - error SetRoleUnauthorized(); - - /// @dev Not authorized, as the caller does not have the role. - error CheckRoleUnauthorized(); - - /// @dev The role is greater than `MAX_ROLE`. - error InvalidRole(); - - /// @dev The user cannot be the zero address. - error UserIsZeroAddress(); - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* PUBLIC WRITE FUNCTIONS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev Sets the status of `role` for `user`. - function setRole(address user, uint8 role, bool active) public payable virtual { - if (msg.sender != _thisOwner()) revert SetRoleUnauthorized(); - _setRole(user, role, active); - } - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* PUBLIC VIEW FUNCTIONS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev Returns all the holders of `role`. - function roleHolders(uint8 role) public view virtual returns (address[] memory) { - return _getEnumerableRolesStorage().holders[role].values(); - } - - /// @dev Returns the number of holders with `role`. - function roleHoldersCount(uint8 role) public view virtual returns (uint256) { - return _getEnumerableRolesStorage().holders[role].length(); - } - - /// @dev Returns the holder of `role` at index `i`. - function roleHoldersAt(uint8 role, uint256 i) public view virtual returns (address) { - return _getEnumerableRolesStorage().holders[role].at(i); - } - - /// @dev Returns the roles of `user`. - function rolesOf(address user) public view virtual returns (uint8[] memory) { - return _getEnumerableRolesStorage().roles[user].values(); - } - - /// @dev Returns if `user` has `role` set to active. - function hasRole(address user, uint8 role) public view virtual returns (bool) { - return _getEnumerableRolesStorage().roles[user].contains(role); - } - - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* INTERNAL HELPERS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev Guards a function such that the caller must have `role`. - modifier onlyRole(uint8 role) virtual { - _checkRole(role); - _; - } - - /// @dev Guards a function such that the caller must be the contact owner or have `role`. - modifier onlyOwnerOrRole(uint8 role) virtual { - _checkOwnerOrRole(role); - _; - } - - /// @dev Guards a function such that the caller must be the contact owner or have `role0` or `role1`. - modifier onlyOwnerOrAnyRole(uint8 role0, uint8 role1) virtual { - _checkOwnerOrAnyRole(role0, role1); - _; - } - - /// @dev Requires that the caller is the contract owner or has `role`. - function _checkOwnerOrAnyRole(uint8 role0, uint8 role1) internal virtual { - if (msg.sender != _thisOwner() && !hasRole(msg.sender, role0)) _checkRole(role1); - } - - /// @dev Requires that the caller is the contract owner or has `role`. - function _checkOwnerOrRole(uint8 role) internal virtual { - if (msg.sender != _thisOwner()) _checkRole(role); - } - - /// @dev Requires that the caller has `role`. - function _checkRole(uint8 role) internal virtual { - if (!hasRole(msg.sender, role)) revert CheckRoleUnauthorized(); - } - - /// @dev Sets the role without authorization checks. - function _setRole(address user, uint8 role, bool active) internal virtual { - if (role > _maxRole()) revert InvalidRole(); - if (user == address(0)) revert UserIsZeroAddress(); - EnumerableRolesStorage storage $ = _getEnumerableRolesStorage(); - if (active) { - $.roles[user].add(role); - $.holders[role].add(user); - } else { - $.roles[user].remove(role); - $.holders[role].remove(user); - } - emit RoleSet(user, role, active); - } - - /// @dev Returns the owner of the contract. - function _thisOwner() internal view virtual returns (address result) { - assembly ("memory-safe") { - mstore(0x00, 0x8da5cb5b) // `owner()`. - result := - mul(mload(0x00), and(gt(returndatasize(), 0x1f), staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20))) - } - } - - /// @dev Returns the maximum valid role. - function _maxRole() internal view virtual returns (uint256 result) { - assembly ("memory-safe") { - mstore(0x00, 0xd24f19d5) // `MAX_ROLE()`. - result := - mul(mload(0x00), and(gt(returndatasize(), 0x1f), staticcall(gas(), address(), 0x1c, 0x04, 0x00, 0x20))) - } - } -} diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index 5b62cb3..b0b5ba7 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {EnumerableRoles} from "clusters/EnumerableRoles.sol"; +import {EnumerableRoles} from "solady/auth/EnumerableRoles.sol"; contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ @@ -11,13 +11,13 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ /// @dev Admin role. - uint8 public constant ADMIN_ROLE = 0; + uint256 public constant ADMIN_ROLE = 0; /// @dev Withdrawer role. - uint8 public constant WITHDRAWER_ROLE = 1; + uint256 public constant WITHDRAWER_ROLE = 1; /// @dev Max role. - uint8 public constant MAX_ROLE = 1; + uint256 public constant MAX_ROLE = 1; /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* WITHDRAW FUNCTIONS */ @@ -26,13 +26,16 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Allows the owner to withdraw ERC20 tokens. function withdrawERC20(address token, address to, uint256 amount) public - onlyOwnerOrAnyRole(ADMIN_ROLE, WITHDRAWER_ROLE) + onlyOwnerOrRoles(abi.encode(ADMIN_ROLE, WITHDRAWER_ROLE)) { SafeTransferLib.safeTransfer(token, to, amount); } /// @dev Allows the owner to withdraw native currency. - function withdrawNative(address to, uint256 amount) public onlyOwnerOrAnyRole(ADMIN_ROLE, WITHDRAWER_ROLE) { + function withdrawNative(address to, uint256 amount) + public + onlyOwnerOrRoles(abi.encode(ADMIN_ROLE, WITHDRAWER_ROLE)) + { SafeTransferLib.safeTransferETH(to, amount); } @@ -66,16 +69,8 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ /// @dev Allow admins to set roles too. - function setRole(address user, uint8 role, bool active) - public - payable - virtual - override - onlyOwnerOrRole(ADMIN_ROLE) - { - _setRole(user, role, active); - } + function _authorizeSetRole(address, uint256, bool) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {} /// @dev For UUPSUpgradeable. Only the owner can upgrade. - function _authorizeUpgrade(address newImplementation) internal override onlyOwnerOrRole(ADMIN_ROLE) {} + function _authorizeUpgrade(address newImplementation) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {} } diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index d75f5e8..88845b8 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -140,7 +140,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ /// @dev Enables the owner to set the destination endpoint ID. - function setDstEid(uint32 eid) public onlyOwnerOrRole(ADMIN_ROLE) { + function setDstEid(uint32 eid) public onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) { _getClustersCommunityInitiatorBetaStorage().dstEid = eid; emit DstEidSet(eid); } diff --git a/test/EnumerableRoles.t.sol b/test/EnumerableRoles.t.sol deleted file mode 100644 index 3ae3bf5..0000000 --- a/test/EnumerableRoles.t.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {LibSort} from "solady/utils/LibSort.sol"; -import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol"; -import "./utils/SoladyTest.sol"; -import "./mocks/MockEnumerableRoles.sol"; - -contract EnumerableRolesTest is SoladyTest { - using DynamicArrayLib for *; - - MockEnumerableRoles internal enumerableRoles; - - function setUp() public { - enumerableRoles = new MockEnumerableRoles(); - } - - function testSetAndGetMaxRole(uint256 value) public { - enumerableRoles.setMaxRole(value); - assertEq(enumerableRoles.maxRole(), value); - } - - function testSetAndGetOwner(address value) public { - enumerableRoles.setOwner(value); - assertEq(enumerableRoles.thisOwner(), value); - } - - function testSetAndGetRoles(bytes32) public { - address user0; - address user1; - do { - user0 = _randomNonZeroAddress(); - user1 = _randomNonZeroAddress(); - } while (user0 == address(0) || user1 == address(0) || user0 == user1); - _testSetAndGetRoles(user0, user1, _sampleRoles(), _sampleRoles()); - } - - function testSetAndGetRoles() public { - uint256[] memory allRoles = DynamicArrayLib.malloc(256); - unchecked { - for (uint256 i; i < 256; ++i) { - allRoles.set(i, i); - } - } - _testSetAndGetRoles(address(1), address(2), allRoles, allRoles); - } - - function _testSetAndGetRoles(address user0, address user1, uint256[] memory user0Roles, uint256[] memory user1Roles) - internal - { - enumerableRoles.setMaxRole(255); - enumerableRoles.setOwner(address(this)); - unchecked { - for (uint256 i; i != user0Roles.length; ++i) { - enumerableRoles.setRole(user0, uint8(user0Roles.get(i)), true); - } - for (uint256 i; i != user1Roles.length; ++i) { - enumerableRoles.setRole(user1, uint8(user1Roles.get(i)), true); - } - _checkRoles(user0, user0Roles); - _checkRoles(user1, user1Roles); - if (_randomChance(32)) { - for (uint256 i; i < 256; ++i) { - if (!_randomChance(8)) continue; - uint8 role = uint8(i); - DynamicArrayLib.DynamicArray memory expected; - if (user0Roles.contains(role)) expected.p(user0); - if (user1Roles.contains(role)) expected.p(user1); - LibSort.sort(expected.data); - address[] memory roleHolders = enumerableRoles.roleHolders(role); - LibSort.sort(roleHolders); - assertEq(abi.encodePacked(expected.data), abi.encodePacked(roleHolders)); - } - } - for (uint256 i; i != user0Roles.length; ++i) { - enumerableRoles.setRole(user0, uint8(user0Roles.get(i)), false); - } - for (uint256 i; i != user1Roles.length; ++i) { - enumerableRoles.setRole(user1, uint8(user1Roles.get(i)), false); - } - assertEq(enumerableRoles.rolesOf(user0).length, 0); - assertEq(enumerableRoles.rolesOf(user1).length, 0); - if (_randomChance(32)) { - for (uint256 i; i < 256; ++i) { - uint8 role = uint8(i); - assertEq(enumerableRoles.roleHolders(role).length, 0); - } - } - } - } - - function _checkRoles(address user, uint256[] memory sampledRoles) internal view { - uint8[] memory roles = enumerableRoles.rolesOf(user); - LibSort.sort(_toUint256Array(roles)); - sampledRoles = LibSort.copy(sampledRoles); - LibSort.sort(sampledRoles); - LibSort.uniquifySorted(sampledRoles); - assertEq(_toUint256Array(roles), sampledRoles); - } - - function _toUint256Array(uint8[] memory a) internal pure returns (uint256[] memory result) { - assembly ("memory-safe") { - result := a - } - } - - function _sampleRoles() internal returns (uint256[] memory roles) { - unchecked { - uint256 n = _random() & 0xf; - roles = DynamicArrayLib.malloc(n); - for (uint256 i; i != n; ++i) { - roles.set(i, _random() & 0xff); - } - } - } -} diff --git a/test/mocks/MockEnumerableRoles.sol b/test/mocks/MockEnumerableRoles.sol deleted file mode 100644 index 913c2b9..0000000 --- a/test/mocks/MockEnumerableRoles.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import "clusters/EnumerableRoles.sol"; - -contract MockEnumerableRoles is EnumerableRoles { - address public owner; - - uint256 public MAX_ROLE; - - function setOwner(address value) public { - owner = value; - } - - function setMaxRole(uint256 value) public { - MAX_ROLE = value; - } - - function thisOwner() public view returns (address) { - return _thisOwner(); - } - - function maxRole() public view returns (uint256) { - return _maxRole(); - } -} From a27f4c07c2e33d0d2a37fa80a6a4109a6f132fa2 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Sun, 13 Oct 2024 09:34:01 +0000 Subject: [PATCH 04/13] Update solady --- lib/solady | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solady b/lib/solady index b81b8e6..c13addd 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit b81b8e62d2a4f34a16578df5a14b270ca5b47cf6 +Subproject commit c13addd0ea539f9c9a3c822cd91c4a6a0b706e03 From d5f57118e586f71559a841acfa58c7b900494b26 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 15 Oct 2024 17:05:21 +0000 Subject: [PATCH 05/13] Add TestERC20 and override for _payNative --- build_create2_deployments.sh | 1 + src/beta/ClustersCommunityInitiatorBeta.sol | 5 +++++ src/beta/TestERC20.sol | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/beta/TestERC20.sol diff --git a/build_create2_deployments.sh b/build_create2_deployments.sh index c303e89..bd4b7ab 100755 --- a/build_create2_deployments.sh +++ b/build_create2_deployments.sh @@ -54,3 +54,4 @@ generateDeployment() { # Generate the deployments. generateDeployment "ClustersCommunityHubBeta"; generateDeployment "ClustersCommunityInitiatorBeta"; +generateDeployment "TestERC20"; diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index 88845b8..95f9de7 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -160,6 +160,11 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } } + /// @dev Override to remove the `if (msg.value != nativeFee) revert()`. + function _payNative(uint256 nativeFee) internal virtual override returns (uint256) { + return nativeFee; + } + /// @dev Returns the default options that encodes `gas`. function _defaultOptions(uint256 gas) internal pure returns (bytes memory) { if (gas >= 1 << 128) revert(); diff --git a/src/beta/TestERC20.sol b/src/beta/TestERC20.sol new file mode 100644 index 0000000..310f64b --- /dev/null +++ b/src/beta/TestERC20.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ERC20} from "solady/tokens/ERC20.sol"; + +contract TestERC20 is ERC20 { + function name() public pure override returns (string memory) { + return "Test"; + } + + function symbol() public pure override returns (string memory) { + return "TEST"; + } + + function mintHundredToSelf() public { + _mint(msg.sender, 100 * 10 ** 18); + } +} From 7fddaaba00f6b6b3b51f6b190412a14c00becd58 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 4 Nov 2024 20:57:53 +0000 Subject: [PATCH 06/13] Refactor with BidConfig struct --- lib/solady | 2 +- src/beta/ClustersCommunityBaseBeta.sol | 119 ++++++++++++++++---- src/beta/ClustersCommunityHubBeta.sol | 68 ++++++----- src/beta/ClustersCommunityInitiatorBeta.sol | 84 ++++---------- 4 files changed, 161 insertions(+), 112 deletions(-) diff --git a/lib/solady b/lib/solady index c13addd..462baeb 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit c13addd0ea539f9c9a3c822cd91c4a6a0b706e03 +Subproject commit 462baebb400df8eb57705e3ac390f69124855cf0 diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index b0b5ba7..da9b819 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -4,8 +4,66 @@ pragma solidity ^0.8.26; import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EnumerableRoles} from "solady/auth/EnumerableRoles.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; + +contract ClustersCommunityBaseVaultBeta { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The caller must be the mothership, and the mothership must be called by the owner. + error Unauthorized(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* WITHDRAW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Allows the owner to withdraw ERC20 tokens. + function withdrawERC20(address mothershipCaller, address token, address to, uint256 amount) public { + _checkMothership(mothershipCaller); + SafeTransferLib.safeTransfer(token, to, amount); + } + + /// @dev Allows the owner to withdraw native currency. + function withdrawNative(address mothershipCaller, address to, uint256 amount) public { + _checkMothership(mothershipCaller); + SafeTransferLib.safeTransferETH(to, amount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Ensures that the caller is the mothership. + function _checkMothership(address mothershipCaller) internal view { + bytes memory args = LibClone.argsOnClone(address(this)); + (address mothership, address vaultOwner) = abi.decode(args, (address, address)); + if (mothership != msg.sender) revert Unauthorized(); + if (vaultOwner != mothershipCaller) revert Unauthorized(); + } +} contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STRUCTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev A struct for passing in a community bid. + struct BidConfig { + // The token. If this is `address(0)`, the token will be the native token. + address token; + // The amount of token. + uint256 amount; + // The recipient of the token payment. + address paymentRecipient; + // The community name. + bytes32 communityName; + // The wallet name, + bytes32 walletName; + // This is the referral address. We use bytes32, as it might be multichain. + bytes32 referralAddress; + } + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* CONSTANTS */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ @@ -19,6 +77,42 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Max role. uint256 public constant MAX_ROLE = 1; + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* IMMUTABLES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Addres of the vault implementation. + address internal immutable _vaultImplementation = address(new ClustersCommunityBaseVaultBeta()); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* VAULT FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the deterministic address of the vault of `vaultOwner`. + function vaultOf(address vaultOwner) public view returns (address) { + bytes memory args = abi.encode(address(this), vaultOwner); + bytes32 salt = keccak256(args); + address deployer = address(this); + return LibClone.predictDeterministicAddress(_vaultImplementation, args, salt, deployer); + } + + /// @dev Creates a vault for `vaultOwner` if one does not exist yet. + function createVault(address vaultOwner) public returns (address instance) { + bytes memory args = abi.encode(address(this), vaultOwner); + bytes32 salt = keccak256(args); + (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, salt); + } + + /// @dev Allows the `vaultOwner` to withdraw ERC20 tokens. + function withdrawERC20OnVault(address vaultOwner, address token, address to, uint256 amount) public { + ClustersCommunityBaseVaultBeta(vaultOf(vaultOwner)).withdrawERC20(msg.sender, token, to, amount); + } + + /// @dev Allows the owner to withdraw native currency. + function withdrawNativeOnVault(address vaultOwner, address to, uint256 amount) public { + ClustersCommunityBaseVaultBeta(vaultOf(vaultOwner)).withdrawNative(msg.sender, to, amount); + } + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* WITHDRAW FUNCTIONS */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ @@ -39,31 +133,6 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { SafeTransferLib.safeTransferETH(to, amount); } - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* INTERNAL HELPERS */ - /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - - /// @dev Returns `a[i]`, without bounds checking. - function _get(address[] calldata a, uint256 i) internal pure returns (address result) { - assembly ("memory-safe") { - result := calldataload(add(a.offset, shl(5, i))) - } - } - - /// @dev Returns `a[i]`, without bounds checking. - function _get(uint256[] calldata a, uint256 i) internal pure returns (uint256 result) { - assembly ("memory-safe") { - result := calldataload(add(a.offset, shl(5, i))) - } - } - - /// @dev Returns `a[i]`, without bounds checking. - function _get(bytes32[] calldata a, uint256 i) internal pure returns (bytes32 result) { - assembly ("memory-safe") { - result := calldataload(add(a.offset, shl(5, i))) - } - } - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* OVERRIDES */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol index abbb49d..80bc57b 100644 --- a/src/beta/ClustersCommunityHubBeta.sol +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -26,7 +26,14 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C /// @dev Emitted when a bid is received. event Bid( - bytes32 from, uint256 tokenChainId, address token, uint256 amount, bytes32 communityName, bytes32 walletName + bytes32 from, + uint256 tokenChainId, + address token, + uint256 amount, + address paymentRecipient, + bytes32 communityName, + bytes32 walletName, + bytes32 referralAddress ); /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ @@ -54,46 +61,55 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C /// @dev Places a bid. /// If any token is `address(0)`, it is treated as the native token. - function placeBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName) - public - payable - nonReentrant - { + function placeBid(BidConfig memory bidConfig) public payable nonReentrant { (bytes32 sender, uint256 chainId) = _senderAndChainId(); if (chainId == block.chainid) { - if (token == address(0)) { - if (msg.value < amount) revert InsufficientNativePayment(); + address vault = createVault(bidConfig.paymentRecipient); + if (bidConfig.token == address(0)) { + if (msg.value < bidConfig.amount) revert InsufficientNativePayment(); + SafeTransferLib.safeTransferETH(vault, bidConfig.amount); } else { - SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); } } - emit Bid(sender, chainId, token, amount, communityName, walletName); + emit Bid( + sender, + chainId, + bidConfig.token, + bidConfig.amount, + bidConfig.paymentRecipient, + bidConfig.communityName, + bidConfig.walletName, + bidConfig.referralAddress + ); } /// @dev Places multiple bids. /// If any token is `address(0)`, it is treated as the native token. - function placeBids( - address[] calldata tokens, - uint256[] calldata amounts, - bytes32[] calldata communityNames, - bytes32[] calldata walletNames - ) public payable nonReentrant { + function placeBids(BidConfig[] memory bidConfigs) public payable nonReentrant { (bytes32 sender, uint256 chainId) = _senderAndChainId(); uint256 requiredNativeValue; - if (tokens.length != amounts.length) revert ArrayLengthsMismatch(); - if (tokens.length != walletNames.length) revert ArrayLengthsMismatch(); - if (tokens.length != communityNames.length) revert ArrayLengthsMismatch(); - for (uint256 i; i < tokens.length; ++i) { - address token = _get(tokens, i); - uint256 amount = _get(amounts, i); + for (uint256 i; i < bidConfigs.length; ++i) { + BidConfig memory bidConfig = bidConfigs[i]; if (chainId == block.chainid) { - if (token == address(0)) { - requiredNativeValue += amount; + address vault = createVault(bidConfig.paymentRecipient); + if (bidConfig.token == address(0)) { + requiredNativeValue += bidConfig.amount; + SafeTransferLib.safeTransferETH(vault, bidConfig.amount); } else { - SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); } } - emit Bid(sender, chainId, token, amount, _get(communityNames, i), _get(walletNames, i)); + emit Bid( + sender, + chainId, + bidConfig.token, + bidConfig.amount, + bidConfig.paymentRecipient, + bidConfig.communityName, + bidConfig.walletName, + bidConfig.referralAddress + ); } if (msg.value < requiredNativeValue) revert InsufficientNativePayment(); } diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index 95f9de7..7925337 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -47,9 +47,6 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /// @dev Insufficient native payment. error InsufficientNativePayment(); - /// @dev The input arrays must have the same length. - error ArrayLengthsMismatch(); - /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* INITIALIZER */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ @@ -66,44 +63,34 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /// @dev Places a bid. /// If any token is `address(0)`, it is treated as the native token. /// All tokens will not be bridged. - function placeBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName, uint256 gas) - public - payable - nonReentrant - { + function placeBid(BidConfig memory bidConfig, uint256 gas) public payable nonReentrant { uint256 requiredNativeValue; - if (token == address(0)) { - requiredNativeValue = amount; + address vault = createVault(bidConfig.paymentRecipient); + if (bidConfig.token == address(0)) { + requiredNativeValue = bidConfig.amount; + SafeTransferLib.safeTransferETH(vault, bidConfig.amount); } else { - SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); } - _sendBids(_encodeBidMessage(token, amount, communityName, walletName), requiredNativeValue, gas); + _sendBids(_encodeBidMessage(bidConfig), requiredNativeValue, gas); } /// @dev Places multiple bids. /// If any token is `address(0)`, it is treated as the native token. /// All tokens will not be bridged. - function placeBids( - address[] calldata tokens, - uint256[] calldata amounts, - bytes32[] calldata communityNames, - bytes32[] calldata walletNames, - uint256 gas - ) public payable nonReentrant { - if (tokens.length != amounts.length) revert ArrayLengthsMismatch(); - if (tokens.length != walletNames.length) revert ArrayLengthsMismatch(); - if (tokens.length != communityNames.length) revert ArrayLengthsMismatch(); + function placeBids(BidConfig[] memory bidConfigs, uint256 gas) public payable nonReentrant { uint256 requiredNativeValue; - for (uint256 i; i < tokens.length; ++i) { - address token = _get(tokens, i); - uint256 amount = _get(amounts, i); - if (token == address(0)) { - requiredNativeValue += amount; + for (uint256 i; i < bidConfigs.length; ++i) { + BidConfig memory bidConfig = bidConfigs[i]; + address vault = createVault(bidConfig.paymentRecipient); + if (bidConfig.token == address(0)) { + requiredNativeValue += bidConfig.amount; + SafeTransferLib.safeTransferETH(vault, bidConfig.amount); } else { - SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount); + SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, address(this), bidConfig.amount); } } - _sendBids(_encodeBidsMessage(tokens, amounts, communityNames, walletNames), requiredNativeValue, gas); + _sendBids(_encodeBidsMessage(bidConfigs), requiredNativeValue, gas); } /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ @@ -116,23 +103,13 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } /// @dev Returns the amount of native gas fee required to place a bid. - function quoteForBid(address token, uint256 amount, bytes32 communityName, bytes32 walletName, uint256 gas) - public - view - returns (uint256) - { - return _quoteNativeFee(_encodeBidMessage(token, amount, communityName, walletName), gas); + function quoteForBid(BidConfig memory bidConfig, uint256 gas) public view returns (uint256) { + return _quoteNativeFee(_encodeBidMessage(bidConfig), gas); } /// @dev Returns the amount of native gas fee required to place the bids. - function quoteForBids( - address[] calldata tokens, - uint256[] calldata amounts, - bytes32[] calldata communityNames, - bytes32[] calldata walletNames, - uint256 gas - ) public view returns (uint256) { - return _quoteNativeFee(_encodeBidsMessage(tokens, amounts, communityNames, walletNames), gas); + function quoteForBids(BidConfig[] memory bidConfigs, uint256 gas) public view returns (uint256) { + return _quoteNativeFee(_encodeBidsMessage(bidConfigs), gas); } /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ @@ -177,33 +154,20 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } /// @dev Encodes the bid calldata. - function _encodeBidMessage(address token, uint256 amount, bytes32 communityName, bytes32 walletName) - internal - view - returns (bytes memory) - { + function _encodeBidMessage(BidConfig memory bidConfig) internal view returns (bytes memory) { return abi.encode( block.chainid, msg.sender, - abi.encodeWithSignature( - "placeBid(address,uint256,bytes32,bytes32)", token, amount, communityName, walletName - ) + abi.encodeWithSignature("placeBid((address,uint256,address,bytes32,bytes32,bytes32))", bidConfig) ); } /// @dev Encodes the bids calldata. - function _encodeBidsMessage( - address[] calldata tokens, - uint256[] calldata amounts, - bytes32[] calldata communityNames, - bytes32[] calldata walletNames - ) internal view returns (bytes memory) { + function _encodeBidsMessage(BidConfig[] memory bidConfigs) internal view returns (bytes memory) { return abi.encode( block.chainid, msg.sender, - abi.encodeWithSignature( - "placeBids(address[],uint256[],bytes32[],bytes32[])", tokens, amounts, communityNames, walletNames - ) + abi.encodeWithSignature("placeBids((address,uint256,address,bytes32,bytes32,bytes32)[])", bidConfigs) ); } } From d8a685d35f4483c91c55a3d677573f1be4809c77 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 4 Nov 2024 21:04:52 +0000 Subject: [PATCH 07/13] Add extra safety check --- src/beta/ClustersCommunityHubBeta.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol index 80bc57b..c09ce3a 100644 --- a/src/beta/ClustersCommunityHubBeta.sol +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -170,6 +170,8 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C if tload(_IN_LZ_RECEIVE_TRANSIENT_SLOT) { sender := tload(_SENDER_TRANSIENT_SLOT) chainId := tload(_CHAIN_ID_TRANSIENT_SLOT) + // Just for extra safety, in case some rogue L2 makes their chain ID the same. + if eq(chainId, chainid()) { invalid() } } } } From 36c71bc4af91ab608b44b29bcba98fa11db481ed Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 4 Nov 2024 21:16:15 +0000 Subject: [PATCH 08/13] Edit comments --- src/beta/ClustersCommunityBaseBeta.sol | 15 ++++++++++++--- src/beta/ClustersCommunityHubBeta.sol | 2 ++ src/beta/ClustersCommunityInitiatorBeta.sol | 2 ++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index da9b819..a5d4656 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -6,6 +6,10 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EnumerableRoles} from "solady/auth/EnumerableRoles.sol"; import {LibClone} from "solady/utils/LibClone.sol"; +/// @title ClustersCommunityBaseVaultBeta +/// @notice A contract to hold ERC20 and native tokens for a recipient of a community bid. +/// We'll just include this within the same file as ClustersCommunityBaseBeta, +/// since it is so tightly coupled. contract ClustersCommunityBaseVaultBeta { /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* ERRORS */ @@ -31,10 +35,11 @@ contract ClustersCommunityBaseVaultBeta { } /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ - /* OVERRIDES */ + /* GUARDS */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - /// @dev Ensures that the caller is the mothership. + /// @dev Ensures that the caller is the mothership, + /// and that the caller of the mothership is the owner of the vault. function _checkMothership(address mothershipCaller) internal view { bytes memory args = LibClone.argsOnClone(address(this)); (address mothership, address vaultOwner) = abi.decode(args, (address, address)); @@ -43,6 +48,10 @@ contract ClustersCommunityBaseVaultBeta { } } +/// @title ClustersCommunityBaseBeta +/// @notice The base class for the ClustersCommunityHubBeta and ClustersCommunityInitiatorBeta. +/// Having a base class helps with conciseness, since both classes share a lot of +/// common logic. contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* STRUCTS */ @@ -81,7 +90,7 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /* IMMUTABLES */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ - /// @dev Addres of the vault implementation. + /// @dev Address of the vault implementation. address internal immutable _vaultImplementation = address(new ClustersCommunityBaseVaultBeta()); /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol index c09ce3a..1ccda63 100644 --- a/src/beta/ClustersCommunityHubBeta.sol +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -6,6 +6,8 @@ import {ReentrancyGuard} from "soledge/utils/ReentrancyGuard.sol"; import {ClustersCommunityBaseBeta} from "clusters/beta/ClustersCommunityBaseBeta.sol"; import {Origin, OAppReceiverUpgradeable} from "layerzero-oapp/contracts/oapp-upgradeable/OAppReceiverUpgradeable.sol"; +/// @title ClustersCommunityHubBeta +/// @notice The hub contract for clusters community sales. contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, ClustersCommunityBaseBeta { /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* TRANSIENT STORAGE */ diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index 7925337..876114b 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -9,6 +9,8 @@ import { OAppSenderUpgradeable, MessagingFee } from "layerzero-oapp/contracts/oapp-upgradeable/OAppSenderUpgradeable.sol"; +/// @title ClustersCommunityHubBeta +/// @notice The initiator contract for clusters community sales. contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuard, ClustersCommunityBaseBeta { using OptionsBuilder for bytes; From 9cf3a052c35eeb5aad924afae46ed65b4f4666c8 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 5 Nov 2024 01:19:39 +0000 Subject: [PATCH 09/13] Optimize --- lib/solady | 2 +- src/beta/ClustersCommunityBaseBeta.sol | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/solady b/lib/solady index 462baeb..d223d43 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 462baebb400df8eb57705e3ac390f69124855cf0 +Subproject commit d223d4347d0866cc7692747edf9c8a7be0d3d047 diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index a5d4656..fa30b6c 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -41,8 +41,9 @@ contract ClustersCommunityBaseVaultBeta { /// @dev Ensures that the caller is the mothership, /// and that the caller of the mothership is the owner of the vault. function _checkMothership(address mothershipCaller) internal view { - bytes memory args = LibClone.argsOnClone(address(this)); - (address mothership, address vaultOwner) = abi.decode(args, (address, address)); + bytes memory args = LibClone.argsOnClone(address(this), 0x00, 0x28); + address mothership = address(bytes20(LibClone.argLoad(args, 0x00))); + address vaultOwner = address(bytes20(LibClone.argLoad(args, 0x14))); if (mothership != msg.sender) revert Unauthorized(); if (vaultOwner != mothershipCaller) revert Unauthorized(); } @@ -99,7 +100,7 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Returns the deterministic address of the vault of `vaultOwner`. function vaultOf(address vaultOwner) public view returns (address) { - bytes memory args = abi.encode(address(this), vaultOwner); + bytes memory args = abi.encodePacked(address(this), vaultOwner); bytes32 salt = keccak256(args); address deployer = address(this); return LibClone.predictDeterministicAddress(_vaultImplementation, args, salt, deployer); @@ -107,7 +108,7 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Creates a vault for `vaultOwner` if one does not exist yet. function createVault(address vaultOwner) public returns (address instance) { - bytes memory args = abi.encode(address(this), vaultOwner); + bytes memory args = abi.encodePacked(address(this), vaultOwner); bytes32 salt = keccak256(args); (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, salt); } From bf68bf0f2470039219b8af9d53950e4d7313ee54 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 5 Nov 2024 01:36:31 +0000 Subject: [PATCH 10/13] Optimize --- src/beta/ClustersCommunityHubBeta.sol | 6 +++--- src/beta/ClustersCommunityInitiatorBeta.sol | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol index 1ccda63..f79988e 100644 --- a/src/beta/ClustersCommunityHubBeta.sol +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -63,7 +63,7 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C /// @dev Places a bid. /// If any token is `address(0)`, it is treated as the native token. - function placeBid(BidConfig memory bidConfig) public payable nonReentrant { + function placeBid(BidConfig calldata bidConfig) public payable nonReentrant { (bytes32 sender, uint256 chainId) = _senderAndChainId(); if (chainId == block.chainid) { address vault = createVault(bidConfig.paymentRecipient); @@ -88,11 +88,11 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C /// @dev Places multiple bids. /// If any token is `address(0)`, it is treated as the native token. - function placeBids(BidConfig[] memory bidConfigs) public payable nonReentrant { + function placeBids(BidConfig[] calldata bidConfigs) public payable nonReentrant { (bytes32 sender, uint256 chainId) = _senderAndChainId(); uint256 requiredNativeValue; for (uint256 i; i < bidConfigs.length; ++i) { - BidConfig memory bidConfig = bidConfigs[i]; + BidConfig calldata bidConfig = bidConfigs[i]; if (chainId == block.chainid) { address vault = createVault(bidConfig.paymentRecipient); if (bidConfig.token == address(0)) { diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index 876114b..ad95ede 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -65,7 +65,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /// @dev Places a bid. /// If any token is `address(0)`, it is treated as the native token. /// All tokens will not be bridged. - function placeBid(BidConfig memory bidConfig, uint256 gas) public payable nonReentrant { + function placeBid(BidConfig calldata bidConfig, uint256 gas) public payable nonReentrant { uint256 requiredNativeValue; address vault = createVault(bidConfig.paymentRecipient); if (bidConfig.token == address(0)) { @@ -80,10 +80,10 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /// @dev Places multiple bids. /// If any token is `address(0)`, it is treated as the native token. /// All tokens will not be bridged. - function placeBids(BidConfig[] memory bidConfigs, uint256 gas) public payable nonReentrant { + function placeBids(BidConfig[] calldata bidConfigs, uint256 gas) public payable nonReentrant { uint256 requiredNativeValue; for (uint256 i; i < bidConfigs.length; ++i) { - BidConfig memory bidConfig = bidConfigs[i]; + BidConfig calldata bidConfig = bidConfigs[i]; address vault = createVault(bidConfig.paymentRecipient); if (bidConfig.token == address(0)) { requiredNativeValue += bidConfig.amount; @@ -105,12 +105,12 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } /// @dev Returns the amount of native gas fee required to place a bid. - function quoteForBid(BidConfig memory bidConfig, uint256 gas) public view returns (uint256) { + function quoteForBid(BidConfig calldata bidConfig, uint256 gas) public view returns (uint256) { return _quoteNativeFee(_encodeBidMessage(bidConfig), gas); } /// @dev Returns the amount of native gas fee required to place the bids. - function quoteForBids(BidConfig[] memory bidConfigs, uint256 gas) public view returns (uint256) { + function quoteForBids(BidConfig[] calldata bidConfigs, uint256 gas) public view returns (uint256) { return _quoteNativeFee(_encodeBidsMessage(bidConfigs), gas); } @@ -156,7 +156,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } /// @dev Encodes the bid calldata. - function _encodeBidMessage(BidConfig memory bidConfig) internal view returns (bytes memory) { + function _encodeBidMessage(BidConfig calldata bidConfig) internal view returns (bytes memory) { return abi.encode( block.chainid, msg.sender, @@ -165,7 +165,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar } /// @dev Encodes the bids calldata. - function _encodeBidsMessage(BidConfig[] memory bidConfigs) internal view returns (bytes memory) { + function _encodeBidsMessage(BidConfig[] calldata bidConfigs) internal view returns (bytes memory) { return abi.encode( block.chainid, msg.sender, From 85af18bc10cb7a6c80a2f3a92d82dc4bab9a614a Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 7 Nov 2024 17:40:25 +0000 Subject: [PATCH 11/13] Edit comment --- src/beta/ClustersCommunityBaseBeta.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index fa30b6c..03bb49b 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -150,6 +150,6 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Allow admins to set roles too. function _authorizeSetRole(address, uint256, bool) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {} - /// @dev For UUPSUpgradeable. Only the owner can upgrade. + /// @dev For UUPSUpgradeable. function _authorizeUpgrade(address newImplementation) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {} } From 6550ba848dad51c59cda2a85cd30aacf0962d8ba Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 7 Nov 2024 18:01:21 +0000 Subject: [PATCH 12/13] Tidy --- src/beta/ClustersCommunityBaseBeta.sol | 34 +++++++++++++-------- src/beta/ClustersCommunityHubBeta.sol | 16 ++-------- src/beta/ClustersCommunityInitiatorBeta.sol | 19 ++---------- 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index 03bb49b..58a1ec6 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -5,6 +5,7 @@ import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EnumerableRoles} from "solady/auth/EnumerableRoles.sol"; import {LibClone} from "solady/utils/LibClone.sol"; +import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol"; /// @title ClustersCommunityBaseVaultBeta /// @notice A contract to hold ERC20 and native tokens for a recipient of a community bid. @@ -41,11 +42,8 @@ contract ClustersCommunityBaseVaultBeta { /// @dev Ensures that the caller is the mothership, /// and that the caller of the mothership is the owner of the vault. function _checkMothership(address mothershipCaller) internal view { - bytes memory args = LibClone.argsOnClone(address(this), 0x00, 0x28); - address mothership = address(bytes20(LibClone.argLoad(args, 0x00))); - address vaultOwner = address(bytes20(LibClone.argLoad(args, 0x14))); - if (mothership != msg.sender) revert Unauthorized(); - if (vaultOwner != mothershipCaller) revert Unauthorized(); + bytes32 h = abi.decode(LibClone.argsOnClone(address(this), 0x00, 0x20), (bytes32)); + if (EfficientHashLib.hash(uint160(msg.sender), uint160(mothershipCaller)) != h) revert Unauthorized(); } } @@ -100,17 +98,14 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Returns the deterministic address of the vault of `vaultOwner`. function vaultOf(address vaultOwner) public view returns (address) { - bytes memory args = abi.encodePacked(address(this), vaultOwner); - bytes32 salt = keccak256(args); - address deployer = address(this); - return LibClone.predictDeterministicAddress(_vaultImplementation, args, salt, deployer); + bytes memory args = abi.encode(EfficientHashLib.hash(uint160(address(this)), uint160(vaultOwner))); + return LibClone.predictDeterministicAddress(_vaultImplementation, args, 0, address(this)); } /// @dev Creates a vault for `vaultOwner` if one does not exist yet. function createVault(address vaultOwner) public returns (address instance) { - bytes memory args = abi.encodePacked(address(this), vaultOwner); - bytes32 salt = keccak256(args); - (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, salt); + bytes memory args = abi.encode(EfficientHashLib.hash(uint160(address(this)), uint160(vaultOwner))); + (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, 0); } /// @dev Allows the `vaultOwner` to withdraw ERC20 tokens. @@ -143,6 +138,21 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { SafeTransferLib.safeTransferETH(to, amount); } + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Transfers the native currency or ERC20 for the bid. + function _transferBidPayment(BidConfig calldata bidConfig) internal returns (uint256 nativeAmount) { + address vault = createVault(bidConfig.paymentRecipient); + if (bidConfig.token == address(0)) { + nativeAmount = bidConfig.amount; + SafeTransferLib.safeTransferETH(vault, nativeAmount); + } else { + SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); + } + } + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ /* OVERRIDES */ /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ diff --git a/src/beta/ClustersCommunityHubBeta.sol b/src/beta/ClustersCommunityHubBeta.sol index f79988e..52dfb03 100644 --- a/src/beta/ClustersCommunityHubBeta.sol +++ b/src/beta/ClustersCommunityHubBeta.sol @@ -66,13 +66,7 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C function placeBid(BidConfig calldata bidConfig) public payable nonReentrant { (bytes32 sender, uint256 chainId) = _senderAndChainId(); if (chainId == block.chainid) { - address vault = createVault(bidConfig.paymentRecipient); - if (bidConfig.token == address(0)) { - if (msg.value < bidConfig.amount) revert InsufficientNativePayment(); - SafeTransferLib.safeTransferETH(vault, bidConfig.amount); - } else { - SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); - } + if (msg.value < _transferBidPayment(bidConfig)) revert InsufficientNativePayment(); } emit Bid( sender, @@ -94,13 +88,7 @@ contract ClustersCommunityHubBeta is OAppReceiverUpgradeable, ReentrancyGuard, C for (uint256 i; i < bidConfigs.length; ++i) { BidConfig calldata bidConfig = bidConfigs[i]; if (chainId == block.chainid) { - address vault = createVault(bidConfig.paymentRecipient); - if (bidConfig.token == address(0)) { - requiredNativeValue += bidConfig.amount; - SafeTransferLib.safeTransferETH(vault, bidConfig.amount); - } else { - SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); - } + requiredNativeValue += _transferBidPayment(bidConfig); } emit Bid( sender, diff --git a/src/beta/ClustersCommunityInitiatorBeta.sol b/src/beta/ClustersCommunityInitiatorBeta.sol index ad95ede..bcacab2 100644 --- a/src/beta/ClustersCommunityInitiatorBeta.sol +++ b/src/beta/ClustersCommunityInitiatorBeta.sol @@ -66,15 +66,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar /// If any token is `address(0)`, it is treated as the native token. /// All tokens will not be bridged. function placeBid(BidConfig calldata bidConfig, uint256 gas) public payable nonReentrant { - uint256 requiredNativeValue; - address vault = createVault(bidConfig.paymentRecipient); - if (bidConfig.token == address(0)) { - requiredNativeValue = bidConfig.amount; - SafeTransferLib.safeTransferETH(vault, bidConfig.amount); - } else { - SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, vault, bidConfig.amount); - } - _sendBids(_encodeBidMessage(bidConfig), requiredNativeValue, gas); + _sendBids(_encodeBidMessage(bidConfig), _transferBidPayment(bidConfig), gas); } /// @dev Places multiple bids. @@ -83,14 +75,7 @@ contract ClustersCommunityInitiatorBeta is OAppSenderUpgradeable, ReentrancyGuar function placeBids(BidConfig[] calldata bidConfigs, uint256 gas) public payable nonReentrant { uint256 requiredNativeValue; for (uint256 i; i < bidConfigs.length; ++i) { - BidConfig calldata bidConfig = bidConfigs[i]; - address vault = createVault(bidConfig.paymentRecipient); - if (bidConfig.token == address(0)) { - requiredNativeValue += bidConfig.amount; - SafeTransferLib.safeTransferETH(vault, bidConfig.amount); - } else { - SafeTransferLib.safeTransferFrom(bidConfig.token, msg.sender, address(this), bidConfig.amount); - } + requiredNativeValue += _transferBidPayment(bidConfigs[i]); } _sendBids(_encodeBidsMessage(bidConfigs), requiredNativeValue, gas); } From 3997893c2a1f8ddef3d0dbcf74c7286ab7a95cdf Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 14 Nov 2024 15:17:46 +0000 Subject: [PATCH 13/13] Add missing receive() to vault implementation --- src/beta/ClustersCommunityBaseBeta.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/beta/ClustersCommunityBaseBeta.sol b/src/beta/ClustersCommunityBaseBeta.sol index 58a1ec6..cbe69ff 100644 --- a/src/beta/ClustersCommunityBaseBeta.sol +++ b/src/beta/ClustersCommunityBaseBeta.sol @@ -45,6 +45,13 @@ contract ClustersCommunityBaseVaultBeta { bytes32 h = abi.decode(LibClone.argsOnClone(address(this), 0x00, 0x20), (bytes32)); if (EfficientHashLib.hash(uint160(msg.sender), uint160(mothershipCaller)) != h) revert Unauthorized(); } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* RECEIVE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev For vault to receive native currency. + receive() external payable {} } /// @title ClustersCommunityBaseBeta @@ -99,23 +106,25 @@ contract ClustersCommunityBaseBeta is EnumerableRoles, UUPSUpgradeable { /// @dev Returns the deterministic address of the vault of `vaultOwner`. function vaultOf(address vaultOwner) public view returns (address) { bytes memory args = abi.encode(EfficientHashLib.hash(uint160(address(this)), uint160(vaultOwner))); - return LibClone.predictDeterministicAddress(_vaultImplementation, args, 0, address(this)); + bytes32 salt; + return LibClone.predictDeterministicAddress(_vaultImplementation, args, salt, address(this)); } /// @dev Creates a vault for `vaultOwner` if one does not exist yet. function createVault(address vaultOwner) public returns (address instance) { bytes memory args = abi.encode(EfficientHashLib.hash(uint160(address(this)), uint160(vaultOwner))); - (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, 0); + bytes32 salt; + (, instance) = LibClone.createDeterministicClone(_vaultImplementation, args, salt); } /// @dev Allows the `vaultOwner` to withdraw ERC20 tokens. function withdrawERC20OnVault(address vaultOwner, address token, address to, uint256 amount) public { - ClustersCommunityBaseVaultBeta(vaultOf(vaultOwner)).withdrawERC20(msg.sender, token, to, amount); + ClustersCommunityBaseVaultBeta(payable(vaultOf(vaultOwner))).withdrawERC20(msg.sender, token, to, amount); } /// @dev Allows the owner to withdraw native currency. function withdrawNativeOnVault(address vaultOwner, address to, uint256 amount) public { - ClustersCommunityBaseVaultBeta(vaultOf(vaultOwner)).withdrawNative(msg.sender, to, amount); + ClustersCommunityBaseVaultBeta(payable(vaultOf(vaultOwner))).withdrawNative(msg.sender, to, amount); } /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/