Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ docs/
# Dotenv file
.env

.DS_Store
.DS_Store

# Create2 build files
.tmp
create2
18 changes: 12 additions & 6 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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/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
57 changes: 57 additions & 0 deletions build_create2_deployments.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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/t)" --etherscan-api-key "na" --show-standard-json-input > "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";
generateDeployment "TestERC20";
2 changes: 1 addition & 1 deletion lib/devtools
Submodule devtools updated 0 files
1 change: 1 addition & 0 deletions lib/multicaller
Submodule multicaller added at 356350
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts-upgradeable
Submodule openzeppelin-contracts-upgradeable updated 59 files
+5 −0 .changeset/fluffy-buses-jump.md
+5 −0 .changeset/great-pianos-work.md
+5 −0 .changeset/odd-lobsters-wash.md
+5 −0 .changeset/serious-carrots-provide.md
+5 −0 .changeset/spotty-queens-own.md
+5 −0 .changeset/tricky-bats-pretend.md
+3 −0 .codecov.yml
+1 −0 .githooks/pre-push
+1 −1 .github/workflows/checks.yml
+1 −1 .github/workflows/formal-verification.yml
+4 −1 CHANGELOG.md
+1 −1 LICENSE
+1 −1 contracts/governance/README.adoc
+3 −0 contracts/interfaces/README.adoc
+1 −1 contracts/metatx/ERC2771ForwarderUpgradeable.sol
+26 −0 contracts/mocks/BatchCallerUpgradeable.sol
+68 −0 contracts/mocks/MerkleProofCustomHashMockUpgradeable.sol
+2 −0 contracts/mocks/StatelessUpgradeable.sol
+28 −0 contracts/mocks/WithInit.sol
+6 −0 contracts/mocks/docs/ERC4626FeesUpgradeable.sol
+44 −0 contracts/mocks/token/ERC20GetterHelperUpgradeable.sol
+2 −0 contracts/token/ERC1155/README.adoc
+5 −0 contracts/token/ERC20/README.adoc
+4 −78 contracts/token/ERC20/extensions/ERC1363Upgradeable.sol
+126 −0 contracts/token/ERC20/extensions/draft-ERC20TemporaryApprovalUpgradeable.sol
+2 −0 contracts/token/ERC721/README.adoc
+12 −4 contracts/utils/README.adoc
+1 −1 docs/modules/ROOT/pages/erc4626.adoc
+101 −8 docs/modules/ROOT/pages/utilities.adoc
+2 −0 foundry.toml
+1 −1 fv-requirements.txt
+19 −21 hardhat.config.js
+1 −1 lib/openzeppelin-contracts
+151 −97 package-lock.json
+4 −4 package.json
+18 −0 scripts/checks/coverage.sh
+4 −1 scripts/generate/run.js
+18 −25 scripts/generate/templates/Arrays.js
+4 −4 scripts/generate/templates/Checkpoints.js
+328 −0 scripts/generate/templates/Heap.js
+13 −0 scripts/generate/templates/Heap.opts.js
+89 −0 scripts/generate/templates/Heap.t.js
+186 −0 scripts/generate/templates/MerkleProof.js
+11 −0 scripts/generate/templates/MerkleProof.opts.js
+25 −14 scripts/generate/templates/Packing.js
+3 −5 scripts/generate/templates/Packing.opts.js
+19 −13 scripts/generate/templates/Packing.t.js
+5 −0 scripts/prepare.sh
+42 −0 test/proxy/Clones.t.sol
+10 −1 test/token/ERC20/ERC20.behavior.js
+142 −0 test/token/ERC20/extensions/draft-ERC20TemporaryApproval.test.js
+3 −3 test/token/ERC20/utils/SafeERC20.test.js
+115 −0 test/utils/Packing.t.sol
+206 −166 test/utils/cryptography/MerkleProof.test.js
+135 −0 test/utils/cryptography/P256.t.sol
+156 −0 test/utils/cryptography/P256.test.js
+3,719 −0 test/utils/cryptography/ecdsa_secp256r1_sha256_p1363_test.json
+153 −0 test/utils/structs/Heap.t.sol
+131 −0 test/utils/structs/Heap.test.js
2 changes: 1 addition & 1 deletion lib/solady
Submodule solady updated 135 files
1 change: 1 addition & 0 deletions lib/soledge
Submodule soledge added at 641f4f
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
174 changes: 174 additions & 0 deletions src/beta/ClustersCommunityBaseBeta.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// 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 "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.
/// We'll just include this within the same file as ClustersCommunityBaseBeta,
/// since it is so tightly coupled.
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);
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* GUARDS */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @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 {
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
/// @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 */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @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 */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @dev Admin role.
uint256 public constant ADMIN_ROLE = 0;

/// @dev Withdrawer role.
uint256 public constant WITHDRAWER_ROLE = 1;

/// @dev Max role.
uint256 public constant MAX_ROLE = 1;

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* IMMUTABLES */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @dev Address 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(EfficientHashLib.hash(uint160(address(this)), uint160(vaultOwner)));
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)));
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(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(payable(vaultOf(vaultOwner))).withdrawNative(msg.sender, to, amount);
}

/*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/
/* WITHDRAW FUNCTIONS */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @dev Allows the owner to withdraw ERC20 tokens.
function withdrawERC20(address token, address to, uint256 amount)
public
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
onlyOwnerOrRoles(abi.encode(ADMIN_ROLE, WITHDRAWER_ROLE))
{
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 */
/*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/

/// @dev Allow admins to set roles too.
function _authorizeSetRole(address, uint256, bool) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {}

/// @dev For UUPSUpgradeable.
function _authorizeUpgrade(address newImplementation) internal override onlyOwnerOrRoles(abi.encode(ADMIN_ROLE)) {}
}
Loading