diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e42bf00 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/ExclusiveDelegateResolver"] + path = contracts/lib/ExclusiveDelegateResolver + url = https://github.com/yuga-labs/ExclusiveDelegateResolver +[submodule "contracts/lib/solady"] + path = contracts/lib/solady + url = https://github.com/vectorized/solady diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml new file mode 100644 index 0000000..762a296 --- /dev/null +++ b/contracts/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/lib/ExclusiveDelegateResolver b/contracts/lib/ExclusiveDelegateResolver new file mode 160000 index 0000000..9bcb15d --- /dev/null +++ b/contracts/lib/ExclusiveDelegateResolver @@ -0,0 +1 @@ +Subproject commit 9bcb15d2470407fd8604140556cf13ccd2bf21c9 diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 0000000..b93cf4b --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66 diff --git a/contracts/lib/solady b/contracts/lib/solady new file mode 160000 index 0000000..8583a6e --- /dev/null +++ b/contracts/lib/solady @@ -0,0 +1 @@ +Subproject commit 8583a6e386b897f3db142a541f86d5953eccd835 diff --git a/contracts/src/AllowlistMint721.sol b/contracts/src/AllowlistMint721.sol new file mode 100644 index 0000000..a14b2c9 --- /dev/null +++ b/contracts/src/AllowlistMint721.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {ERC721} from "solady/tokens/ERC721.sol"; +import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; +import {IExclusiveDelegateResolver} from "ExclusiveDelegateResolver/src/IExclusiveDelegateResolver.sol"; +// @dev use zkSync specific import for signature verification +import {SignatureCheckerLib} from "solady/utils/ext/zksync/SignatureCheckerLib.sol"; + +contract AllowlistMint721 is ERC721 { + error AlreadyMinted(); + error NotAllowed(); + error InvalidProof(); + error InvalidSignature(); + + bytes24 constant _AGW_LINK_RIGHTS = bytes24(keccak256("AGW_LINK")); + + IExclusiveDelegateResolver public constant DELEGATE_RESOLVER = + IExclusiveDelegateResolver(0x0000000078CC4Cc1C14E27c0fa35ED6E5E58825D); + + bytes32 public merkleRoot; + address public exampleSigner; + + mapping(address => bool) public minted; + + uint256 public tokenIndex; + + constructor(bytes32 _merkleRoot, address _exampleSigner) { + merkleRoot = _merkleRoot; + exampleSigner = _exampleSigner; + } + + function name() public pure override returns (string memory) { + return "Example"; + } + + function symbol() public pure override returns (string memory) { + return "EXAMPLE"; + } + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + return ""; + } + + function mintWithMerkleProof(address user, bytes32[] calldata proof) public { + // check if the address has already minted + if (minted[user]) revert AlreadyMinted(); + + // in this example, we want to allow the user ONLY to mint from a delegated AGW if it is set + // check that the caller is the current exclusively delegated wallet by rights + if (msg.sender != DELEGATE_RESOLVER.exclusiveWalletByRights(user, _AGW_LINK_RIGHTS)) { + revert NotAllowed(); + } + + // check the proof against the passed address as that is the address submitted + // for allowlisting (likely an EOA wallet) + if (!MerkleProofLib.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(user)))) { + revert InvalidProof(); + } + + minted[user] = true; + _mint(msg.sender, ++tokenIndex); + } + + function mintWithSignature(address user, bytes calldata signature) public { + if (minted[user]) revert AlreadyMinted(); + + // in this example, we want to allow the user to mint for themselves OR from a delegated AGW + // if the caller is not the user we are minting for; check if the caller is the exclusively + // delegated wallet for AGW linking + if (msg.sender != user) { + address allowedWallet = DELEGATE_RESOLVER.exclusiveWalletByRights(user, _AGW_LINK_RIGHTS); + if (msg.sender != allowedWallet) { + revert NotAllowed(); + } + } + + // check the signature against the passed address as that is the address submitted + // for allowlisting (likely an EOA wallet) + // @dev this is not a particular secure method of signature verification; would recommend using + // a more robust EIP-712 typed signature instead of just signing the hashed address + if (!SignatureCheckerLib.isValidSignatureNow(exampleSigner, keccak256(abi.encode(user)), signature)) { + revert InvalidSignature(); + } + + minted[user] = true; + _mint(msg.sender, ++tokenIndex); + } +} diff --git a/contracts/src/ERC20ClaimForHolders.sol b/contracts/src/ERC20ClaimForHolders.sol new file mode 100644 index 0000000..36d34a2 --- /dev/null +++ b/contracts/src/ERC20ClaimForHolders.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IExclusiveDelegateResolver} from "ExclusiveDelegateResolver/src/IExclusiveDelegateResolver.sol"; +import {LibBitmap} from "solady/utils/LibBitmap.sol"; +// @dev use zkSync specific import for safe transfer +import {SafeTransferLib} from "solady/utils/ext/zksync/SafeTransferLib.sol"; + +contract ERC20ClaimForHolders { + using SafeTransferLib for address; + using LibBitmap for LibBitmap.Bitmap; + + error NotExclusiveOwnerByRights(); + error AlreadyClaimed(); + + uint256 public constant TOKENS_PER_NFT = 1e18; + address public NFT_CONTRACT_ADDRESS; + address public ERC20_CONTRACT_ADDRESS; + + bytes24 constant _AGW_LINK_RIGHTS = bytes24(keccak256("AGW_LINK")); + + IExclusiveDelegateResolver public constant DELEGATE_RESOLVER = + IExclusiveDelegateResolver(0x0000000078CC4Cc1C14E27c0fa35ED6E5E58825D); + + LibBitmap.Bitmap private _claimedTokens; + + function claimTokens(uint256 tokenId) external { + // check that the caller is the current exclusively delegated owner of this NFT by rights. + // If they are not, we are not going to let them claim the tokens. + // An alternative approach here would be to check that they are either the owner of the NFT + // and if they are not, check the delegation resolver to see if they are delegated to claim the tokens. + address owner = DELEGATE_RESOLVER.exclusiveOwnerByRights(NFT_CONTRACT_ADDRESS, tokenId, _AGW_LINK_RIGHTS); + if (msg.sender != owner) { + revert NotExclusiveOwnerByRights(); + } + + if (isClaimed(tokenId)) { + revert AlreadyClaimed(); + } + + _claimedTokens.set(tokenId); + + ERC20_CONTRACT_ADDRESS.safeTransfer(msg.sender, TOKENS_PER_NFT); + } + + function isClaimed(uint256 tokenId) public view returns (bool) { + return _claimedTokens.get(tokenId); + } +}