diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bec87b2..7f9bb82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ name: CI + on: workflow_dispatch: pull_request: @@ -7,24 +8,39 @@ on: - 'main' jobs: - test: + uv-example: + name: python runs-on: ubuntu-latest + env: + UV_SYSTEM_PYTHON: 1 + steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.5.26' + + - name: 'Set up Python' + uses: actions/setup-python@v5 with: python-version-file: 'pyproject.toml' - - name: Install dependencies + - name: Install project and dependencies run: uv sync --all-extras --dev - - uses: astral-sh/ruff-action@v3 - - 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 contract dependencies + run: ape pm install gh:Vectorized/solady --name solady --ref v0.1.3 + + - name: Compile contracts + run: ape compile --force - name: Run tests run: ape test -s diff --git a/.gitignore b/.gitignore index 2d97033..9bfe6db 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ wheels/ # Virtual environments .venv -# Unit test -.pytest_cache/ \ No newline at end of file +# Ape +.pytest_cache/ +.build/ \ No newline at end of file diff --git a/ape-config.yaml b/ape-config.yaml index c4ff42d..e8a6a9d 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -9,3 +9,11 @@ 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/contracts/TokenAllowlist.sol b/contracts/TokenAllowlist.sol new file mode 100644 index 0000000..03e9149 --- /dev/null +++ b/contracts/TokenAllowlist.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +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; + + constructor() { + _initializeOwner(msg.sender); + } + + function addToken(address token) external onlyOwner { + allowList.add(token); + } + + function addTokensBatch(address[] memory tokens) external onlyOwner { + for (uint256 i; i < tokens.length; ++i) { + allowList.add(tokens[i]); + } + } + + function removeToken(address token) external onlyOwner { + allowList.remove(token); + } + + function removeTokensBatch(address[] memory tokens) external onlyOwner { + for (uint256 i; i < tokens.length; ++i) { + allowList.remove(tokens[i]); + } + } + + function allowedTokens() external view returns (address[] memory) { + return allowList.values(); + } + + function isAllowed(address token) external view returns (bool) { + return allowList.contains(token); + } +} diff --git a/hello.py b/hello.py deleted file mode 100644 index 5d63b85..0000000 --- a/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from cow-agent!") - - -if __name__ == "__main__": - main() diff --git a/tests/test_add.py b/tests/test_add.py deleted file mode 100644 index 48fe0df..0000000 --- a/tests/test_add.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_add(): - assert 1 + 1 == 2 diff --git a/tests/test_allowlist.py b/tests/test_allowlist.py new file mode 100644 index 0000000..b57b218 --- /dev/null +++ b/tests/test_allowlist.py @@ -0,0 +1,105 @@ +import ape +import pytest + + +# Gnosis chain token addresses +class TokenAddresses: + GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" + COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" + WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + 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)