From c0e72d01fb24f90a2ec4ff4510d12dd653fa3b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 24 Jan 2025 20:38:05 -0500 Subject: [PATCH 1/5] chore: forge init --- contracts/.github/workflows/test.yml | 45 +++++++++++++++++++ contracts/.gitignore | 14 ++++++ contracts/README.md | 66 ++++++++++++++++++++++++++++ contracts/foundry.toml | 6 +++ contracts/script/Counter.s.sol | 19 ++++++++ contracts/src/Counter.sol | 14 ++++++ contracts/test/Counter.t.sol | 24 ++++++++++ 7 files changed, 188 insertions(+) create mode 100644 contracts/.github/workflows/test.yml create mode 100644 contracts/.gitignore create mode 100644 contracts/README.md create mode 100644 contracts/foundry.toml create mode 100644 contracts/script/Counter.s.sol create mode 100644 contracts/src/Counter.sol create mode 100644 contracts/test/Counter.t.sol 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/script/Counter.s.sol b/contracts/script/Counter.s.sol new file mode 100644 index 0000000..cdc1fe9 --- /dev/null +++ b/contracts/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol new file mode 100644 index 0000000..aded799 --- /dev/null +++ b/contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol new file mode 100644 index 0000000..54b724f --- /dev/null +++ b/contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From db216e4faf7f5a9dd2e9c40625fbdc16e9069bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 24 Jan 2025 20:38:06 -0500 Subject: [PATCH 2/5] forge install: forge-std v1.9.5 --- .gitmodules | 3 +++ contracts/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 contracts/lib/forge-std diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c65a596 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std 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 From 295e58571978b6cd2c733953d878f1098874fc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 24 Jan 2025 20:39:01 -0500 Subject: [PATCH 3/5] forge install: ExclusiveDelegateResolver --- .gitmodules | 3 +++ contracts/lib/ExclusiveDelegateResolver | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/ExclusiveDelegateResolver diff --git a/.gitmodules b/.gitmodules index c65a596..b1a3380 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [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 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 From 30f24c13f8dc1ec3f3f7baa5b70a632e6762c647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 24 Jan 2025 20:39:16 -0500 Subject: [PATCH 4/5] forge install: solady v0.0.298 --- .gitmodules | 3 +++ contracts/lib/solady | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/solady diff --git a/.gitmodules b/.gitmodules index b1a3380..e42bf00 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [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/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 From 4a327403da86c86042fd4b13e40da3a874445dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 24 Jan 2025 21:02:37 -0500 Subject: [PATCH 5/5] add solidity AGW linking examples --- contracts/script/Counter.s.sol | 19 ------ contracts/src/AllowlistMint721.sol | 89 ++++++++++++++++++++++++++ contracts/src/Counter.sol | 14 ---- contracts/src/ERC20ClaimForHolders.sol | 49 ++++++++++++++ contracts/test/Counter.t.sol | 24 ------- 5 files changed, 138 insertions(+), 57 deletions(-) delete mode 100644 contracts/script/Counter.s.sol create mode 100644 contracts/src/AllowlistMint721.sol delete mode 100644 contracts/src/Counter.sol create mode 100644 contracts/src/ERC20ClaimForHolders.sol delete mode 100644 contracts/test/Counter.t.sol diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/contracts/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} 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/Counter.sol b/contracts/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/contracts/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} 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); + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/contracts/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}