diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 999a9d1..96e6991 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,30 +8,6 @@ on: - 'main' jobs: - lint: - name: 'Smart Contract Infra Lint' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./smart-contract-infra - steps: - - name: 'Checkout the repo' - uses: actions/checkout@v4 - - - name: 'Install Bun' - uses: oven-sh/setup-bun@v1 - - - name: 'Install the node.js dependencies' - run: bun install - - - name: 'Lint the code' - run: bun run lint - - - name: 'Add lint summary' - run: | - echo "## Lint result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - trader-lint: name: 'CoW Trader Lint' runs-on: 'ubuntu-latest' @@ -63,85 +39,82 @@ jobs: echo "## Lint result" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - trader-build: - name: 'CoW Trader Build' + contracts-lint: + name: 'Smart Contract Lint' runs-on: 'ubuntu-latest' defaults: run: - working-directory: './cow-trader' + working-directory: './smart-contract-infra' steps: - - name: 'Checkout the repo' + - name: 'Check out the repo' uses: 'actions/checkout@v4' - - name: 'Install UV' - uses: 'astral-sh/setup-uv@v5' - with: - version: '0.5.26' + - name: 'Install Foundry' + uses: 'foundry-rs/foundry-toolchain@v1' - - name: 'Setup Python' - uses: 'actions/setup-python@v5' - with: - python-version-file: './cow-trader/pyproject.toml' + - name: 'Install Bun' + uses: 'oven-sh/setup-bun@v1' - - name: 'Install dependencies' - run: 'uv sync --all-extras --dev' + - name: 'Install the Node.js dependencies' + run: 'bun install' - - name: 'Install Ape' - uses: 'ApeWorX/github-action@v3' - with: - python-version: '3.11' - ape-version-pin: '0.8.25' - ape-plugins-list: 'solidity==0.8.5 alchemy==0.8.7 etherscan==0.8.4 foundry==0.8.7' + - name: 'Lint the code' + run: 'bun run lint' - - name: 'Install contract dependencies' - run: 'ape pm install gh:Vectorized/solady --name solady --ref v0.1.3' + - name: 'Add lint summary' + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - name: 'Compile contracts' - run: 'ape compile --force' + contracts-build: + name: 'Smart Contract Build' + runs-on: 'ubuntu-latest' + defaults: + run: + working-directory: './smart-contract-infra' + steps: + - name: 'Check out the repo' + uses: 'actions/checkout@v4' + + - name: 'Install Foundry' + uses: 'foundry-rs/foundry-toolchain@v1' + + - name: 'Install Bun' + uses: 'oven-sh/setup-bun@v1' + + - name: 'Install the Node.js dependencies' + run: 'bun install' + + - name: 'Build the contracts and print their size' + run: 'forge build --sizes' - name: 'Add build summary' run: | echo "## Build result" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - trader-test: - name: 'CoW Trader Test' - needs: ['trader-lint', 'trader-build'] + contracts-test: + name: 'Smart Contract Test' + needs: ['contracts-lint', 'contracts-build'] runs-on: 'ubuntu-latest' - env: - UV_SYSTEM_PYTHON: 1 defaults: run: - working-directory: './cow-trader' + working-directory: './smart-contract-infra' steps: - - name: 'Checkout the repo' + - name: 'Check out the repo' uses: 'actions/checkout@v4' - - name: 'Install UV' - uses: 'astral-sh/setup-uv@v5' - with: - version: '0.5.26' + - name: 'Install Foundry' + uses: 'foundry-rs/foundry-toolchain@v1' - - name: 'Setup Python' - uses: 'actions/setup-python@v5' - with: - python-version-file: './cow-trader/pyproject.toml' - - - name: 'Install dependencies' - run: 'uv sync --all-extras --dev' - - - name: 'Install Ape' - uses: 'ApeWorX/github-action@v3' - with: - python-version: '3.11' - ape-version-pin: '0.8.25' - ape-plugins-list: 'solidity==0.8.5 alchemy==0.8.7 etherscan==0.8.4 foundry==0.8.7' + - name: 'Install Bun' + uses: 'oven-sh/setup-bun@v1' - - name: 'Install contract dependencies' - run: 'ape pm install gh:Vectorized/solady --name solady --ref v0.1.3' + - name: 'Install the Node.js dependencies' + run: 'bun install' - - name: 'Run tests' - run: 'ape test -s' + - name: 'Run the tests' + run: 'forge test' - name: 'Add test summary' run: | diff --git a/.gitignore b/.gitignore index 97b02e7..2e7d66d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,48 +1,44 @@ -# Silverback -__pycache__/ -*.py[oc] +# Directories +.build/ +.cache +.db/ +.idea/ +.npm +.nyc_output/ +.pytest_cache/ +.silverback* +.venv +.vscode/ +.yarn/ +abi/ build/ +cache/ +coverage/ dist/ +node_modules/ +out/ wheels/ -*.egg-info -.venv -.pytest_cache/ -.build/ -.silverback* -.ruff* -# Bun -node_modules/ -dist/ +# Files +*.egg-info +*.env +*.lcov +*.log +*.py[oc] *.tsbuildinfo -coverage/ -.npm +.DS_Store .eslintcache -.yarn/ .pnp.* -.cache - -# Logs -logs/ -*.log +.ruff* +lcov.info npm-debug.log* +package-lock.json +pnpm-lock.yaml yarn-debug.log* yarn-error.log* +yarn.lock -# Environment -.env -.env.* - -# IDE/OS -.vscode/ -.idea/ -.DS_Store - -# Project specific -.db/ -abi/ - -# Test coverage -coverage/ -.nyc_output/ -*.lcov \ No newline at end of file +# Broadcasts +!broadcast +broadcast/* +broadcast/*/31337/ \ No newline at end of file diff --git a/cow-trader/ape-config.yaml b/cow-trader/ape-config.yaml index e8a6a9d..c4ff42d 100644 --- a/cow-trader/ape-config.yaml +++ b/cow-trader/ape-config.yaml @@ -9,11 +9,3 @@ plugins: version: 0.8.7 - name: solidity version: 0.8.5 - -solidity: - version: 0.8.25 - -dependencies: - - name: Solady - github: Vectorized/solady - ref: v0.1.3 diff --git a/cow-trader/tests/test_allowlist.py b/cow-trader/tests/test_allowlist.py deleted file mode 100644 index 1a5d367..0000000 --- a/cow-trader/tests/test_allowlist.py +++ /dev/null @@ -1,105 +0,0 @@ -import ape -import pytest - - -# Gnosis chain token addresses -class TokenAddresses: - GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" - COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" - WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1" - SAFE = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" - - -@pytest.fixture -def tokens(): - return TokenAddresses() - - -@pytest.fixture -def token_batch(tokens): - return [tokens.GNO, tokens.COW, tokens.WETH, tokens.SAFE] - - -@pytest.fixture -def owner(accounts): - return accounts[0] - - -@pytest.fixture -def not_owner(accounts): - return accounts[1] - - -@pytest.fixture -def allowlist_contract(owner, project): - return owner.deploy(project.TokenAllowlist) - - -@pytest.fixture -def populated_allowlist(allowlist_contract, owner, token_batch): - allowlist_contract.addTokensBatch(token_batch, sender=owner) - return allowlist_contract - - -def test_add_token(allowlist_contract, tokens, owner, not_owner): - allowlist_contract.addToken(tokens.GNO, sender=owner) - allowed = allowlist_contract.isAllowed(tokens.GNO) - assert allowed is True - - with ape.reverts(): - allowlist_contract.addToken(tokens.GNO, sender=not_owner) - - -def test_token_not_allowed(allowlist_contract, tokens, owner): - allowlist_contract.addToken(tokens.GNO, sender=owner) - allowed = allowlist_contract.isAllowed(tokens.COW) - assert allowed is False - - -def test_add_batch_tokens(allowlist_contract, token_batch, owner, not_owner): - allowlist_contract.addTokensBatch(token_batch, sender=owner) - for token in token_batch: - allowed = allowlist_contract.isAllowed(token) - assert allowed is True - - with ape.reverts(): - allowlist_contract.addTokensBatch(token_batch, sender=not_owner) - - -def test_allowed_tokens(allowlist_contract, token_batch, owner): - allowlist_contract.addTokensBatch(token_batch, sender=owner) - allowed_tokens = allowlist_contract.allowedTokens() - assert allowed_tokens == token_batch - - -def test_remove_token(populated_allowlist, tokens, owner, not_owner): - populated_allowlist.removeToken(tokens.GNO, sender=owner) - allowed = populated_allowlist.isAllowed(tokens.GNO) - assert allowed is False - - expected_tokens = [tokens.COW, tokens.WETH, tokens.SAFE] - allowed_tokens = populated_allowlist.allowedTokens() - assert set(allowed_tokens) == set(expected_tokens) - - with ape.reverts(): - populated_allowlist.removeToken(tokens.GNO, sender=not_owner) - - -def test_remove_batch_tokens(populated_allowlist, tokens, owner, not_owner): - tokens_to_remove = [tokens.GNO, tokens.WETH] - populated_allowlist.removeTokensBatch(tokens_to_remove, sender=owner) - - for token in tokens_to_remove: - allowed = populated_allowlist.isAllowed(token) - assert allowed is False - - remaining_tokens = [tokens.COW, tokens.SAFE] - for token in remaining_tokens: - allowed = populated_allowlist.isAllowed(token) - assert allowed is True - - allowed_tokens = populated_allowlist.allowedTokens() - assert set(allowed_tokens) == set(remaining_tokens) - - with ape.reverts(): - populated_allowlist.removeTokensBatch(tokens_to_remove, sender=not_owner) diff --git a/smart-contract-infra/.prettierignore b/smart-contract-infra/.prettierignore index 3a89310..2a77960 100644 --- a/smart-contract-infra/.prettierignore +++ b/smart-contract-infra/.prettierignore @@ -1,5 +1,11 @@ -node_modules +# directories +broadcast +cache coverage +node_modules +out + +# files *.env *.log .DS_Store @@ -7,5 +13,5 @@ coverage bun.lockb lcov.info package-lock.json -yarn.lock -cow-trader/ \ No newline at end of file +pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/smart-contract-infra/.solhint.json b/smart-contract-infra/.solhint.json new file mode 100644 index 0000000..1603dc4 --- /dev/null +++ b/smart-contract-infra/.solhint.json @@ -0,0 +1,22 @@ +{ + "extends": "solhint:recommended", + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.28"], + "func-name-mixedcase": "off", + "func-visibility": [ + "error", + { + "ignoreConstructors": true + } + ], + "max-line-length": ["error", 120], + "named-parameters-mapping": "warn", + "no-console": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off", + "custom-errors": "off", + "reason-string": "off", + "no-empty-blocks": "off" + } +} diff --git a/smart-contract-infra/README.md b/smart-contract-infra/README.md index e74684b..227cef0 100644 --- a/smart-contract-infra/README.md +++ b/smart-contract-infra/README.md @@ -1,15 +1 @@ -# smart-contract-infra - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.31. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. +## Smart Contract Infrastructure diff --git a/smart-contract-infra/bun.lockb b/smart-contract-infra/bun.lockb index bcb25bb..47ff29e 100755 Binary files a/smart-contract-infra/bun.lockb and b/smart-contract-infra/bun.lockb differ diff --git a/smart-contract-infra/foundry.toml b/smart-contract-infra/foundry.toml new file mode 100644 index 0000000..49d458b --- /dev/null +++ b/smart-contract-infra/foundry.toml @@ -0,0 +1,35 @@ +[profile.default] +auto_detect_solc = false +block_timestamp = 1_738_368_000 # Feb 1, 2025 at 00:00 GMT +fuzz = { runs = 1_000 } +optimizer = true +optimizer_runs = 10_000 +out = "out" +script = "script" +solc = "0.8.28" +src = "src" + +[profile.ci] +fuzz = { runs = 10_000 } +verbosity = 4 + +[etherscan] +mainnet = { key = "${API_KEY_ETHERSCAN}" } + +[fmt] +bracket_spacing = true +int_types = "long" +line_length = 120 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 4 +wrap_comments = true + +[rpc_endpoints] +arbitrum = "https://rpc.ankr.com/arbitrum" +base = "https://rpc.ankr.com/base" +gnosis = "https://rpc.ankr.com/gnosis" +localhost = "http://localhost:8545" +mainnet = "https://rpc.ankr.com/eth" +sepolia = "https://rpc2.sepolia.org" diff --git a/smart-contract-infra/package.json b/smart-contract-infra/package.json index 1890b5a..2db47dc 100644 --- a/smart-contract-infra/package.json +++ b/smart-contract-infra/package.json @@ -2,13 +2,27 @@ "name": "smart-contract-infra", "module": "index.ts", "type": "module", + "dependencies": { + "@gnosis-guild/zodiac": "4.0.3", + "@gnosis.pm/safe-contracts": "1.3.0", + "@openzeppelin/contracts-upgradeable": "5.2.0", + "cowprotocol/contracts": "github:cowprotocol/contracts#9c1984b", + "solady": "0.1.6" + }, "devDependencies": { - "@types/bun": "latest", - "prettier": "^3.0.0" + "forge-std": "github:foundry-rs/forge-std#v1.8.1", + "prettier": "^3.0.0", + "solhint": "^3.6.2" }, "scripts": { - "lint": "bun run prettier:check", + "clean": "rm -rf cache out", + "build": "forge build", + "lint": "bun run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && bun solhint \"{script,src,tests}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"", - "prettier:write": "prettier --write \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"" + "prettier:write": "prettier --write \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"", + "test": "forge test", + "test:coverage": "forge coverage", + "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" } } diff --git a/smart-contract-infra/remappings.txt b/smart-contract-infra/remappings.txt new file mode 100644 index 0000000..5cc0e5c --- /dev/null +++ b/smart-contract-infra/remappings.txt @@ -0,0 +1,7 @@ +forge-std/=node_modules/forge-std/src/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@gnosis.pm/safe-contracts/=node_modules/@gnosis.pm/safe-contracts/ +@gnosis-guild/zodiac/=node_modules/@gnosis-guild/zodiac/ +solady/=node_modules/solady/ +cowprotocol/=node_modules/cowprotocol/ \ No newline at end of file diff --git a/smart-contract-infra/src/CoWSwapGuard.sol b/smart-contract-infra/src/CoWSwapGuard.sol new file mode 100644 index 0000000..83803fe --- /dev/null +++ b/smart-contract-infra/src/CoWSwapGuard.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import { ITokenAllowlist } from "src/interfaces/ITokenAllowlist.sol"; +import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; +import { BaseGuard } from "@gnosis-guild/zodiac/contracts/guard/BaseGuard.sol"; + +contract CoWSwapGuard is BaseGuard { + /// @notice Thrown when the target address is not GPv2Settlement contract address + error InvalidAddress(); + + /// @notice Thrown when the function selector is not `setPreSignature(bytes,bool)` + error InvalidSelector(); + + /// @notice Thrown when either the buy or sell token for an order is not allowed + error OrderNotAllowed(); + + /// @notice GPv2Settlement address + /// @dev Deterministically deployed + address internal constant GPV2_SETTLEMENT_ADDRESS = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + + /// @notice Function selector for GPv2Settlement.setPreSignature + bytes4 internal constant SET_PRE_SIGNATURE_SELECTOR = 0xec6cb13f; + + /// @notice CoW DAO's governance controlled token allowlist contract + ITokenAllowlist internal immutable ALLOWLIST; + + constructor(address _allowlist) { + ALLOWLIST = ITokenAllowlist(_allowlist); + } + + /// @notice Checks the trade is on CoW Swap for allowed tokens and sets the pre-signed order tradeability + /// @dev Module transactions only use the first four parameters: to, value, data, and operation. + function checkTransaction( + address to, + uint256, + bytes memory data, + Enum.Operation, + uint256, + uint256, + uint256, + address, + address payable, + bytes memory, + address + ) + external + view + override + { + require(to == GPV2_SETTLEMENT_ADDRESS, InvalidAddress()); + + (bytes memory txData, address sellToken, address buyToken) = abi.decode(data, (bytes, address, address)); + + require(ALLOWLIST.isOrderAllowed(sellToken, buyToken), OrderNotAllowed()); + + bytes32 selector; + assembly { + selector := mload(add(txData, 32)) + } + require(selector == SET_PRE_SIGNATURE_SELECTOR, InvalidSelector()); + } + + function checkAfterExecution(bytes32 txHash, bool success) external override { } +} diff --git a/cow-trader/contracts/TokenAllowlist.sol b/smart-contract-infra/src/TokenAllowlist.sol similarity index 51% rename from cow-trader/contracts/TokenAllowlist.sol rename to smart-contract-infra/src/TokenAllowlist.sol index 03e9149..9307f7d 100644 --- a/cow-trader/contracts/TokenAllowlist.sol +++ b/smart-contract-infra/src/TokenAllowlist.sol @@ -1,43 +1,47 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.25; +pragma solidity 0.8.28; -import {Ownable} from "solady/src/auth/Ownable.sol"; -import {EnumerableSetLib} from "solady/src/utils/EnumerableSetLib.sol"; +import { Ownable } from "solady/src/auth/Ownable.sol"; +import { EnumerableSetLib } from "solady/src/utils/EnumerableSetLib.sol"; contract TokenAllowlist is Ownable { using EnumerableSetLib for EnumerableSetLib.AddressSet; - EnumerableSetLib.AddressSet internal allowList; + EnumerableSetLib.AddressSet internal allowlist; constructor() { _initializeOwner(msg.sender); } function addToken(address token) external onlyOwner { - allowList.add(token); + allowlist.add(token); } function addTokensBatch(address[] memory tokens) external onlyOwner { for (uint256 i; i < tokens.length; ++i) { - allowList.add(tokens[i]); + allowlist.add(tokens[i]); } } function removeToken(address token) external onlyOwner { - allowList.remove(token); + allowlist.remove(token); } function removeTokensBatch(address[] memory tokens) external onlyOwner { for (uint256 i; i < tokens.length; ++i) { - allowList.remove(tokens[i]); + allowlist.remove(tokens[i]); } } function allowedTokens() external view returns (address[] memory) { - return allowList.values(); + return allowlist.values(); } - function isAllowed(address token) external view returns (bool) { - return allowList.contains(token); + function isAllowed(address token) public view returns (bool) { + return allowlist.contains(token); + } + + function isOrderAllowed(address sellToken, address buyToken) external view returns (bool) { + return isAllowed(sellToken) && isAllowed(buyToken); } } diff --git a/smart-contract-infra/src/TradingModule.sol b/smart-contract-infra/src/TradingModule.sol new file mode 100644 index 0000000..18310da --- /dev/null +++ b/smart-contract-infra/src/TradingModule.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; +import { Module } from "@gnosis-guild/zodiac/contracts/core/Module.sol"; +import { Guardable } from "@gnosis-guild/zodiac/contracts/guard/Guardable.sol"; +import { GPv2Order } from "cowprotocol/contracts/src/contracts/libraries/GPv2Order.sol"; +import { GPv2Signing } from "cowprotocol/contracts/src/contracts/mixins/GPv2Signing.sol"; + +import { IAvatar } from "@gnosis-guild/zodiac/contracts/interfaces/IAvatar.sol"; +import { IGuard } from "@gnosis-guild/zodiac/contracts/interfaces/IGuard.sol"; + +abstract contract TradingModule is Module, Guardable { + using GPv2Order for bytes; + + /// @notice Emitted when an order was successfully set + event SetOrder(bytes indexed orderUid, bool indexed signed); + + /// @notice Thrown when the supplied order UID and order is mismatched + error InvalidOrderUID(); + + /// @notice Thrown when the transaction cannot execute + error CannotExec(); + + /// @notice GPv2Settlement address + /// @dev Deterministically deployed + address public constant GPV2_SETTLEMENT_ADDRESS = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + + /// @notice CoW Swap Guard contract address + address public constant COW_SWAP_GUARD = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; // placeholder + + /// @notice GPv2Settlement domain separator + bytes32 internal domainSeparator; + + constructor(address _owner, address _avatar, address _target) { + bytes memory initParams = abi.encode(_owner, _avatar, _target); + setUp(initParams); + } + + function setUp(bytes memory initParams) public override initializer { + (address _owner, address _avatar, address _target) = abi.decode(initParams, (address, address, address)); + + require(_avatar != address(0)); + require(_target != address(0)); + + __Ownable_init(msg.sender); + + setAvatar(_avatar); + setTarget(_target); + + domainSeparator = GPv2Signing(GPV2_SETTLEMENT_ADDRESS).domainSeparator(); + + transferOwnership(_owner); + } + + function setOrder(bytes memory orderUid, GPv2Order.Data memory order, bool signed) external { + bytes memory uid = new bytes(GPv2Order.UID_LENGTH); + uid.packOrderUidParams(GPv2Order.hash(order, domainSeparator), owner(), order.validTo); + require(keccak256(orderUid) == keccak256(uid), InvalidOrderUID()); + + bytes memory txData = abi.encodeCall(GPv2Signing.setPreSignature, (orderUid, signed)); + bytes memory data = abi.encode(txData, address(order.sellToken), address(order.buyToken)); + + require(exec(GPV2_SETTLEMENT_ADDRESS, 0, data, Enum.Operation.Call), CannotExec()); + + emit SetOrder(orderUid, signed); + } + + function exec( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation + ) + internal + override + returns (bool) + { + IGuard(COW_SWAP_GUARD).checkTransaction( + to, value, data, operation, 0, 0, 0, address(0), payable(0), "", msg.sender + ); + + (bytes memory txData,,) = abi.decode(data, (bytes, address, address)); + + return IAvatar(target).execTransactionFromModule(to, value, txData, operation); + } +} diff --git a/smart-contract-infra/src/interfaces/ITokenAllowlist.sol b/smart-contract-infra/src/interfaces/ITokenAllowlist.sol new file mode 100644 index 0000000..6db9348 --- /dev/null +++ b/smart-contract-infra/src/interfaces/ITokenAllowlist.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +interface ITokenAllowlist { + function isOrderAllowed(address sellToken, address buyToken) external view returns (bool); +} diff --git a/smart-contract-infra/test/Base.t.sol b/smart-contract-infra/test/Base.t.sol new file mode 100644 index 0000000..3f7f2bb --- /dev/null +++ b/smart-contract-infra/test/Base.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import { TokenAllowlist } from "src/TokenAllowlist.sol"; +import { Constants } from "test/utils/Constants.sol"; +import { Utils } from "test/utils/Utils.sol"; + +contract BaseTest is Constants, Utils { + address internal alice; + address internal dao; + + TokenAllowlist internal allowlist; + + function setUp() public { + alice = makeAddr("alice"); + dao = makeAddr("dao"); + + vm.startPrank(dao); + allowlist = new TokenAllowlist(); + + // Default caller: dao + } +} diff --git a/smart-contract-infra/test/unit/TokenAllowlist.t.sol b/smart-contract-infra/test/unit/TokenAllowlist.t.sol new file mode 100644 index 0000000..329e251 --- /dev/null +++ b/smart-contract-infra/test/unit/TokenAllowlist.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import { BaseTest } from "test/Base.t.sol"; + +contract Concrete_Unit_TokenAllowlist is BaseTest { + function test_ShouldRevert_AddToken() external { + resetPrank(alice); + vm.expectRevert(); + allowlist.addToken(GNO); + } + + modifier whenOwner() { + _; + } + + function test_AddToken() external whenOwner { + allowlist.addToken(GNO); + assertEq(allowlist.isAllowed(GNO), true); + } +} diff --git a/smart-contract-infra/test/utils/Constants.sol b/smart-contract-infra/test/utils/Constants.sol new file mode 100644 index 0000000..7737faa --- /dev/null +++ b/smart-contract-infra/test/utils/Constants.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +contract Constants { + /// @notice Gnosis Chain GNO address + address internal constant GNO = 0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb; + + /// @notice Gnosis Chain COW address + address internal constant COW = 0x177127622c4A00F3d409B75571e12cB3c8973d3c; + + /// @notice Gnosis Chain WETH address + address internal constant WETH = 0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1; + + /// @notice Gnosis Chain SAFE address + address internal constant SAFE = 0x5aFE3855358E112B5647B952709E6165e1c1eEEe; +} diff --git a/smart-contract-infra/test/utils/Utils.sol b/smart-contract-infra/test/utils/Utils.sol new file mode 100644 index 0000000..1383c49 --- /dev/null +++ b/smart-contract-infra/test/utils/Utils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import { Test } from "forge-std/Test.sol"; + +contract Utils is Test { + /// @notice Stops the active prank and starts a new one with `_msgSender` + function resetPrank(address _msgSender) internal { + vm.stopPrank(); + vm.startPrank(_msgSender); + } +}