diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd84be16..ac7562a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,11 @@ name: SX-Starknet Workflow +env: + STARKNET_SIERRA_COMPILE_PATH: ./cairo/bin/starknet-sierra-compile + OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES + ADDRESS: "0x347be35996a21f6bf0623e75dbce52baba918ad5ae8d83b6f416045ab22961a" + PK: "0xbdd640fb06671ad11c80317fa3b1799d" + on: push: branches: @@ -14,28 +20,28 @@ jobs: name: Forge tests runs-on: ubuntu-latest steps: - - name: Step 1 - Check out main branch + - name: Check out main branch uses: actions/checkout@v3 with: submodules: recursive - - name: Step 2 - Install Foundry + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - - name: Step 3 - Check formatting + - name: Check formatting working-directory: ./ethereum run: forge fmt --check - - name: Step 4 - Build Solidity contracts + - name: Build Solidity contracts working-directory: ./ethereum run: | forge --version forge build --sizes id: build - - name: Step 5 - Run Forge tests + - name: Run Forge tests working-directory: ./ethereum run: | forge test -vvv @@ -46,17 +52,54 @@ jobs: fail-fast: true name: Cairo tests - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - - name: Step 1 - Check out main branch + - name: Check out main branch uses: actions/checkout@v3 - - name: Step 2 - Install Scarb + - name: Set up node + uses: actions/setup-node@v3 + with: + node-version: 18.16.0 + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache Yarn dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- + + - name: Install Yarn dependencies + run: yarn install + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: Install Python dependencies + run: | + sudo apt install -y libgmp3-dev + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install Cairo + run: curl -L https://github.com/starkware-libs/cairo/releases/download/v2.2.0/release-x86_64-unknown-linux-musl.tar.gz > cairo.tar.gz + + - name: Extract Cairo + run: tar -xvf cairo.tar.gz + + - name: Install Scarb uses: software-mansion/setup-scarb@v1 with: scarb-version: 0.7.0 - - name: Step 3 - Check formatting + - name: Step 3 - Check Cairo formatting working-directory: ./starknet run: scarb fmt --check @@ -64,6 +107,12 @@ jobs: working-directory: ./starknet run: scarb build --verbose - - name: Step 4 - Running Cairo tests + - name: Step 4 - Run Cairo tests working-directory: ./starknet - run: scarb test --verbose \ No newline at end of file + run: scarb test --verbose + + - name: Compile Cairo contracts for Hardhat tests + run: yarn hardhat starknet-build + + - name: run Hardhat tests + run: yarn test:l1-execution \ No newline at end of file diff --git a/audited_cairo_libfuncs.json b/audited_cairo_libfuncs.json new file mode 100644 index 00000000..1a8e9df2 --- /dev/null +++ b/audited_cairo_libfuncs.json @@ -0,0 +1,160 @@ +{ + "allowed_libfuncs": [ + "alloc_local", + "array_append", + "array_get", + "array_len", + "array_new", + "array_pop_front", + "array_pop_front_consume", + "array_slice", + "array_snapshot_pop_back", + "array_snapshot_pop_front", + "bitwise", + "bool_and_impl", + "bool_not_impl", + "bool_or_impl", + "bool_to_felt252", + "bool_xor_impl", + "branch_align", + "call_contract_syscall", + "class_hash_const", + "class_hash_to_felt252", + "class_hash_try_from_felt252", + "contract_address_const", + "contract_address_to_felt252", + "contract_address_try_from_felt252", + "deploy_syscall", + "disable_ap_tracking", + "downcast", + "drop", + "dup", + "ec_neg", + "ec_point_from_x_nz", + "ec_point_is_zero", + "ec_point_try_new_nz", + "ec_point_unwrap", + "ec_point_zero", + "ec_state_add", + "ec_state_add_mul", + "ec_state_init", + "ec_state_try_finalize_nz", + "emit_event_syscall", + "enable_ap_tracking", + "enum_init", + "enum_match", + "enum_snapshot_match", + "felt252_add", + "felt252_const", + "felt252_dict_entry_finalize", + "felt252_dict_entry_get", + "felt252_dict_new", + "felt252_dict_squash", + "felt252_div", + "felt252_is_zero", + "felt252_mul", + "felt252_sub", + "finalize_locals", + "function_call", + "get_block_hash_syscall", + "get_builtin_costs", + "get_execution_info_syscall", + "hades_permutation", + "into_box", + "jump", + "keccak_syscall", + "library_call_syscall", + "match_nullable", + "null", + "nullable_from_box", + "pedersen", + "rename", + "replace_class_syscall", + "revoke_ap_tracking", + "secp256k1_add_syscall", + "secp256k1_get_xy_syscall", + "secp256k1_get_point_from_x_syscall", + "secp256k1_mul_syscall", + "secp256k1_new_syscall", + "send_message_to_l1_syscall", + "snapshot_take", + "storage_address_from_base", + "storage_address_from_base_and_offset", + "storage_address_to_felt252", + "storage_address_try_from_felt252", + "storage_base_address_const", + "storage_base_address_from_felt252", + "storage_read_syscall", + "storage_write_syscall", + "store_local", + "store_temp", + "struct_construct", + "struct_deconstruct", + "struct_snapshot_deconstruct", + "u256_safe_divmod", + "u256_sqrt", + "u256_is_zero", + "u128_const", + "u128_eq", + "u128_is_zero", + "u128_overflowing_add", + "u128_overflowing_sub", + "u128_safe_divmod", + "u128_sqrt", + "u128_byte_reverse", + "u128_to_felt252", + "u128_guarantee_mul", + "u128_mul_guarantee_verify", + "u128s_from_felt252", + "u16_bitwise", + "u16_const", + "u16_eq", + "u16_is_zero", + "u16_overflowing_add", + "u16_overflowing_sub", + "u16_safe_divmod", + "u16_sqrt", + "u16_to_felt252", + "u16_try_from_felt252", + "u16_wide_mul", + "u32_bitwise", + "u32_const", + "u32_eq", + "u32_is_zero", + "u32_overflowing_add", + "u32_overflowing_sub", + "u32_safe_divmod", + "u32_sqrt", + "u32_to_felt252", + "u32_try_from_felt252", + "u32_wide_mul", + "u512_safe_divmod_by_u256", + "u64_bitwise", + "u64_const", + "u64_eq", + "u64_is_zero", + "u64_overflowing_add", + "u64_overflowing_sub", + "u64_safe_divmod", + "u64_sqrt", + "u64_to_felt252", + "u64_try_from_felt252", + "u64_wide_mul", + "u8_bitwise", + "u8_const", + "u8_eq", + "u8_is_zero", + "u8_overflowing_add", + "u8_overflowing_sub", + "u8_safe_divmod", + "u8_sqrt", + "u8_to_felt252", + "u8_try_from_felt252", + "u8_wide_mul", + "unbox", + "unwrap_non_zero", + "upcast", + "withdraw_gas", + "withdraw_gas_all" + ] +} \ No newline at end of file diff --git a/ethereum/src/deps.sol b/ethereum/src/deps.sol index a428f735..c87cf349 100644 --- a/ethereum/src/deps.sol +++ b/ethereum/src/deps.sol @@ -4,3 +4,4 @@ pragma solidity ^0.8.19; import "@safe-global/safe-contracts/contracts/SafeL2.sol"; import "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol"; +import "@gnosis.pm/zodiac/contracts/factory/ModuleProxyFactory.sol"; \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 2d09880d..4fb4702f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -44,11 +44,11 @@ const config: HardhatUserConfig = { }, networks: { ethereumLocal: { - url: 'http://localhost:8545', + url: 'http://127.0.0.1:8545/', chainId: 31337, }, starknetLocal: { - url: 'http://localhost:5050', + url: 'http://127.0.0.1:5050', }, }, gasReporter: { @@ -63,6 +63,7 @@ const config: HardhatUserConfig = { network: 'starknetLocal', recompile: false, venv: 'active', + requestTimeout: 90_000, }, paths: { starknetSources: './starknet', diff --git a/package.json b/package.json index ebcf951f..d4149f93 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,14 @@ "repository": "https://github.com/snapshot-labs/sx-starknet-2.git", "author": "Snapshot Labs", "license": "MIT", + "main": "index.js", "scripts": { "format:ts": "eslint . --ext .ts --fix", - "test:stark-sig-auth": "jest -c jest.config.ts" + "chain:l1": "hardhat node", + "chain:l2": "bash './scripts/chain-l2.sh'", + "chain": "yarn chain:l1 & yarn chain:l2", + "test:stark-sig-auth": "jest -c jest.config.ts", + "test:l1-execution": "bash './scripts/l1-execution.sh'" }, "devDependencies": { "@gnosis.pm/zodiac": "^3.3.7", @@ -27,8 +32,9 @@ "@types/sinon-chai": "^3.2.9", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", + "axios": "^1.5.0", "chai": "^4.3.7", - "concurrently": "^8.2.1", + "concurrently": "^7.0.0", "dotenv": "^16.3.1", "eslint": "^8.46.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..671bb8a3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +https://github.com/starkware-libs/cairo-lang/releases/download/v0.12.2/cairo-lang-0.12.2.zip +starknet-devnet==0.6.1 + diff --git a/scripts/chain-l2.sh b/scripts/chain-l2.sh new file mode 100644 index 00000000..86d8cc62 --- /dev/null +++ b/scripts/chain-l2.sh @@ -0,0 +1,3 @@ +#!/bin/bash +starknet-devnet --seed 42 --verbose --sierra-compiler-path "${STARKNET_SIERRA_COMPILE_PATH}" --compiler-args '--allowed-libfuncs-list-file ./audited_cairo_libfuncs.json --add-pythonic-hints' --lite-mode +exit 0 diff --git a/scripts/l1-execution.sh b/scripts/l1-execution.sh index 9fdac406..b234b5b1 100644 --- a/scripts/l1-execution.sh +++ b/scripts/l1-execution.sh @@ -1,7 +1,9 @@ #!/bin/bash -yarn wait-on tcp:5050 && -yarn wait-on tcp:8545 && +kill -9 $(lsof -t -i:8545) +kill -9 $(lsof -t -i:5050) +yarn chain & +sleep 10 && yarn hardhat test starknet/test/l1-execution.test.ts --network 'ethereumLocal' --starknet-network 'starknetLocal' if [ $? -eq 0 ] then diff --git a/starknet/test/l1-execution.test.ts b/starknet/test/l1-execution.test.ts index 9fb5fb60..c2808fc1 100644 --- a/starknet/test/l1-execution.test.ts +++ b/starknet/test/l1-execution.test.ts @@ -1,21 +1,27 @@ import dotenv from 'dotenv'; +import axios from 'axios'; import { expect } from 'chai'; -import { starknet, ethers } from 'hardhat'; -import { Provider, Account, CallData, Uint256, uint256 } from 'starknet'; +import { starknet, ethers, network } from 'hardhat'; +import { HttpNetworkConfig } from 'hardhat/types'; +import { CallData, Uint256, uint256 } from 'starknet'; import { safeWithL1AvatarExecutionStrategySetup } from './utils'; dotenv.config(); -const eth_network = process.env.ETH_NETWORK_URL || ''; -const stark_network = process.env.STARKNET_NETWORK_URL || ''; +const eth_network: string = (network.config as HttpNetworkConfig).url; const account_address = process.env.ADDRESS || ''; const account_pk = process.env.PK || ''; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); +async function increaseEthBlockchainTime(seconds: number) { + await axios({ + method: 'post', + url: eth_network, + data: { id: 1337, jsonrpc: '2.0', method: 'evm_increaseTime', params: [seconds] }, + }); } describe('L1 Avatar Execution', function () { + console.log(eth_network); this.timeout(1000000); let signer: ethers.Wallet; @@ -23,10 +29,7 @@ describe('L1 Avatar Execution', function () { let mockStarknetMessaging: ethers.Contract; let l1AvatarExecutionStrategy: ethers.Contract; - // Using both a starknet hardhat and sn.js account wrapper as hardhat has cleaner deployment flow - // but sn.js has cleaner contract interactions. Syntax is being integrated soon. - let account: Account; - let accountSH: starknet.starknetAccount; + let account: starknet.starknetAccount; let starkTxAuthenticator: starknet.StarknetContract; let vanillaVotingStrategy: starknet.StarknetContract; let vanillaProposalValidationStrategy: starknet.StarknetContract; @@ -34,20 +37,11 @@ describe('L1 Avatar Execution', function () { let ethRelayer: starknet.StarknetContract; before(async function () { - accountSH = await starknet.OpenZeppelinAccount.getAccountFromAddress( - account_address, - account_pk, - ); - - account = new Account( - new Provider({ sequencer: { baseUrl: stark_network } }), - account_address, - account_pk, - ); - const signers = await ethers.getSigners(); signer = signers[0]; + account = await starknet.OpenZeppelinAccount.getAccountFromAddress(account_address, account_pk); + const starkTxAuthenticatorFactory = await starknet.getContractFactory( 'sx_StarkTxAuthenticator', ); @@ -62,20 +56,20 @@ describe('L1 Avatar Execution', function () { try { // If the contracts are already declared, this will be skipped - await accountSH.declare(starkTxAuthenticatorFactory); - await accountSH.declare(vanillaVotingStrategyFactory); - await accountSH.declare(vanillaProposalValidationStrategyFactory); - await accountSH.declare(ethRelayerFactory); - await accountSH.declare(spaceFactory); + await account.declare(starkTxAuthenticatorFactory); + await account.declare(vanillaVotingStrategyFactory); + await account.declare(vanillaProposalValidationStrategyFactory); + await account.declare(ethRelayerFactory); + await account.declare(spaceFactory); } catch {} - starkTxAuthenticator = await accountSH.deploy(starkTxAuthenticatorFactory); - vanillaVotingStrategy = await accountSH.deploy(vanillaVotingStrategyFactory); - vanillaProposalValidationStrategy = await accountSH.deploy( + starkTxAuthenticator = await account.deploy(starkTxAuthenticatorFactory); + vanillaVotingStrategy = await account.deploy(vanillaVotingStrategyFactory); + vanillaProposalValidationStrategy = await account.deploy( vanillaProposalValidationStrategyFactory, ); - ethRelayer = await accountSH.deploy(ethRelayerFactory); - space = await accountSH.deploy(spaceFactory); + ethRelayer = await account.deploy(ethRelayerFactory); + space = await account.deploy(spaceFactory); // Initializing the space const initializeCalldata = CallData.compile({ @@ -94,11 +88,8 @@ describe('L1 Avatar Execution', function () { _metadata_URI: [], _dao_URI: [], }); - await account.execute({ - contractAddress: space.address, - entrypoint: 'initialize', - calldata: initializeCalldata, - }); + + await account.invoke(space, 'initialize', initializeCalldata, { rawInput: true }); const quorum = 1; @@ -113,13 +104,12 @@ describe('L1 Avatar Execution', function () { ethRelayer.address, quorum, )); + // Dumping the Starknet state so it can be loaded at the same point for each test await starknet.devnet.dump('dump.pkl'); }, 10000000); it('should execute a proposal via the Avatar Execution Strategy connected to a Safe', async function () { - // // Resetting Starknet Devnet Timestamp - // await starknet.devnet.setTime(Date.now()/1000); await starknet.devnet.restart(); await starknet.devnet.load('./dump.pkl'); await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); @@ -149,10 +139,10 @@ describe('L1 Avatar Execution', function () { ]; // Propose - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_propose', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ space: space.address, author: account.address, executionStrategy: { @@ -162,13 +152,14 @@ describe('L1 Avatar Execution', function () { userProposalValidationParams: [], metadataURI: [], }), - }); + { rawInput: true }, + ); // Vote - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_vote', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_vote', + CallData.compile({ space: space.address, voter: account.address, proposalId: { low: '0x1', high: '0x0' }, @@ -176,21 +167,23 @@ describe('L1 Avatar Execution', function () { userVotingStrategies: [{ index: '0x0', params: [] }], metadataURI: [], }), - }); + { rawInput: true }, + ); - // TODO: Advance time so that the maxVotingTimestamp is exceeded + // Advance time so that the maxVotingTimestamp is exceeded await starknet.devnet.increaseTime(10); - await sleep(10000); + await increaseEthBlockchainTime(10); // Execute - await account.execute({ - contractAddress: space.address, - entrypoint: 'execute', - calldata: CallData.compile({ + await account.invoke( + space, + 'execute', + CallData.compile({ proposalId: { low: '0x1', high: '0x0' }, executionPayload: executionPayload, }), - }); + { rawInput: true }, + ); // Propogating message to L1 const flushL2Response = await starknet.devnet.flush(); @@ -236,7 +229,7 @@ describe('L1 Avatar Execution', function () { ); }, 10000000); - it('should revert execution if quorum is not met', async function () { + it('should revert execution if quorum is not met (abstain votes only)', async function () { await starknet.devnet.restart(); await starknet.devnet.load('./dump.pkl'); await starknet.devnet.increaseTime(10); @@ -267,10 +260,10 @@ describe('L1 Avatar Execution', function () { ]; // Propose - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_propose', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ space: space.address, author: account.address, executionStrategy: { @@ -280,13 +273,134 @@ describe('L1 Avatar Execution', function () { userProposalValidationParams: [], metadataURI: [], }), + { rawInput: true }, + ); + + // Vote + await account.invoke( + starkTxAuthenticator, + 'authenticate_vote', + CallData.compile({ + space: space.address, + voter: account.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x2', + userVotingStrategies: [{ index: '0x0', params: [] }], + metadataURI: [], + }), + { rawInput: true }, + ); + + // Advance time so that the maxVotingTimestamp is exceeded + await starknet.devnet.increaseTime(10); + await increaseEthBlockchainTime(10); + + // Execute + await account.invoke( + space, + 'execute', + CallData.compile({ + proposalId: { low: '0x1', high: '0x0' }, + executionPayload: executionPayload, + }), + { rawInput: true }, + ); + + // Propogating message to L1 + const flushL2Response = await starknet.devnet.flush(); + const message_payload = flushL2Response.consumed_messages.from_l2[0].payload; + // Proposal data can either be extracted from the message sent to L1 (as done here) or from the pulled from the contract directly + const space_message = message_payload[0]; + const proposal = { + startTimestamp: message_payload[1], + minEndTimestamp: message_payload[2], + maxEndTimestamp: message_payload[3], + finalizationStatus: message_payload[4], + executionPayloadHash: message_payload[5], + executionStrategy: message_payload[6], + authorAddressType: message_payload[7], + author: message_payload[8], + activeVotingStrategies: uint256.uint256ToBN({ + low: message_payload[9], + high: message_payload[10], + }), + }; + // Sending an invalid forVotes value + const forVotes = 10; + const againstVotes = uint256.uint256ToBN({ + low: message_payload[13], + high: message_payload[14], + }); + const abstainVotes = uint256.uint256ToBN({ + low: message_payload[15], + high: message_payload[16], }); + await expect( + l1AvatarExecutionStrategy.execute( + space_message, + proposal, + forVotes, + againstVotes, + abstainVotes, + executionHash, + [proposalTx], + ), + ).to.be.revertedWith('INVALID_MESSAGE_TO_CONSUME'); + }, 10000000); + + it('should revert execution if quorum is not met (abstain votes only)', async function () { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const proposalTx = { + to: signer.address, + value: 0, + data: '0x22', + operation: 0, + salt: 1, + }; + + const abiCoder = new ethers.utils.AbiCoder(); + const executionHash = ethers.utils.keccak256( + abiCoder.encode( + ['tuple(address to, uint256 value, bytes data, uint8 operation, uint256 salt)[]'], + [[proposalTx]], + ), + ); + // Represent the execution hash as a Cairo Uint256 + const executionHashUint256: Uint256 = uint256.bnToUint256(executionHash); + + const executionPayload = [ + l1AvatarExecutionStrategy.address, + executionHashUint256.low, + executionHashUint256.high, + ]; + + // Propose + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ + space: space.address, + author: account.address, + executionStrategy: { + address: ethRelayer.address, + params: executionPayload, + }, + userProposalValidationParams: [], + metadataURI: [], + }), + { rawInput: true }, + ); + // Vote - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_vote', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_vote', + CallData.compile({ space: space.address, voter: account.address, proposalId: { low: '0x1', high: '0x0' }, @@ -294,22 +408,257 @@ describe('L1 Avatar Execution', function () { userVotingStrategies: [{ index: '0x0', params: [] }], metadataURI: [], }), + { rawInput: true }, + ); + + // Advance time so that the maxVotingTimestamp is exceeded + await starknet.devnet.increaseTime(10); + await increaseEthBlockchainTime(10); + + // Execute + await account.invoke( + space, + 'execute', + CallData.compile({ + proposalId: { low: '0x1', high: '0x0' }, + executionPayload: executionPayload, + }), + { rawInput: true }, + ); + + // Propogating message to L1 + const flushL2Response = await starknet.devnet.flush(); + const message_payload = flushL2Response.consumed_messages.from_l2[0].payload; + // Proposal data can either be extracted from the message sent to L1 (as done here) or from the pulled from the contract directly + const space_message = message_payload[0]; + const proposal = { + startTimestamp: message_payload[1], + minEndTimestamp: message_payload[2], + maxEndTimestamp: message_payload[3], + finalizationStatus: message_payload[4], + executionPayloadHash: message_payload[5], + executionStrategy: message_payload[6], + authorAddressType: message_payload[7], + author: message_payload[8], + activeVotingStrategies: uint256.uint256ToBN({ + low: message_payload[9], + high: message_payload[10], + }), + }; + const forVotes = uint256.uint256ToBN({ + low: message_payload[11], + high: message_payload[12], + }); + const againstVotes = uint256.uint256ToBN({ + low: message_payload[13], + high: message_payload[14], }); + const abstainVotes = uint256.uint256ToBN({ + low: message_payload[15], + high: message_payload[16], + }); + + // For some reason CI fails with revertedWith('InvalidProposalStatus') but works locally. + await expect( + l1AvatarExecutionStrategy.execute( + space_message, + proposal, + forVotes, + againstVotes, + abstainVotes, + executionHash, + [proposalTx], + ), + ).to.be.reverted; + }, 10000000); + + it('should revert execution if quorum is not met (against votes only)', async function () { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const proposalTx = { + to: signer.address, + value: 0, + data: '0x22', + operation: 0, + salt: 1, + }; + + const abiCoder = new ethers.utils.AbiCoder(); + const executionHash = ethers.utils.keccak256( + abiCoder.encode( + ['tuple(address to, uint256 value, bytes data, uint8 operation, uint256 salt)[]'], + [[proposalTx]], + ), + ); + // Represent the execution hash as a Cairo Uint256 + const executionHashUint256: Uint256 = uint256.bnToUint256(executionHash); - // TODO: Advance time so that the maxVotingTimestamp is exceeded - await starknet.devnet.increaseTime(20); - await sleep(20000); + const executionPayload = [ + l1AvatarExecutionStrategy.address, + executionHashUint256.low, + executionHashUint256.high, + ]; + + // Propose + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ + space: space.address, + author: account.address, + executionStrategy: { + address: ethRelayer.address, + params: executionPayload, + }, + userProposalValidationParams: [], + metadataURI: [], + }), + { rawInput: true }, + ); + + // Vote + await account.invoke( + starkTxAuthenticator, + 'authenticate_vote', + CallData.compile({ + space: space.address, + voter: account.address, + proposalId: { low: '0x1', high: '0x0' }, + choice: '0x0', + userVotingStrategies: [{ index: '0x0', params: [] }], + metadataURI: [], + }), + { rawInput: true }, + ); + + // Advance time so that the maxVotingTimestamp is exceeded + await starknet.devnet.increaseTime(10); + await increaseEthBlockchainTime(10); // Execute - await account.execute({ - contractAddress: space.address, - entrypoint: 'execute', - calldata: CallData.compile({ + await account.invoke( + space, + 'execute', + CallData.compile({ proposalId: { low: '0x1', high: '0x0' }, executionPayload: executionPayload, }), + { rawInput: true }, + ); + + // Propogating message to L1 + const flushL2Response = await starknet.devnet.flush(); + const message_payload = flushL2Response.consumed_messages.from_l2[0].payload; + // Proposal data can either be extracted from the message sent to L1 (as done here) or from the pulled from the contract directly + const space_message = message_payload[0]; + const proposal = { + startTimestamp: message_payload[1], + minEndTimestamp: message_payload[2], + maxEndTimestamp: message_payload[3], + finalizationStatus: message_payload[4], + executionPayloadHash: message_payload[5], + executionStrategy: message_payload[6], + authorAddressType: message_payload[7], + author: message_payload[8], + activeVotingStrategies: uint256.uint256ToBN({ + low: message_payload[9], + high: message_payload[10], + }), + }; + const forVotes = uint256.uint256ToBN({ + low: message_payload[11], + high: message_payload[12], + }); + const againstVotes = uint256.uint256ToBN({ + low: message_payload[13], + high: message_payload[14], + }); + const abstainVotes = uint256.uint256ToBN({ + low: message_payload[15], + high: message_payload[16], }); + // For some reason CI fails with revertedWith('InvalidProposalStatus') but works locally. + await expect( + l1AvatarExecutionStrategy.execute( + space_message, + proposal, + forVotes, + againstVotes, + abstainVotes, + executionHash, + [proposalTx], + ), + ).to.be.reverted; + }, 10000000); + + it('should revert execution if quorum is not met (no votes)', async function () { + await starknet.devnet.restart(); + await starknet.devnet.load('./dump.pkl'); + await starknet.devnet.increaseTime(10); + await starknet.devnet.loadL1MessagingContract(eth_network, mockStarknetMessaging.address); + + const proposalTx = { + to: signer.address, + value: 0, + data: '0x22', + operation: 0, + salt: 1, + }; + + const abiCoder = new ethers.utils.AbiCoder(); + const executionHash = ethers.utils.keccak256( + abiCoder.encode( + ['tuple(address to, uint256 value, bytes data, uint8 operation, uint256 salt)[]'], + [[proposalTx]], + ), + ); + // Represent the execution hash as a Cairo Uint256 + const executionHashUint256: Uint256 = uint256.bnToUint256(executionHash); + + const executionPayload = [ + l1AvatarExecutionStrategy.address, + executionHashUint256.low, + executionHashUint256.high, + ]; + + // Propose + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ + space: space.address, + author: account.address, + executionStrategy: { + address: ethRelayer.address, + params: executionPayload, + }, + userProposalValidationParams: [], + metadataURI: [], + }), + { rawInput: true }, + ); + + // No Vote Cast + + // Advance time so that the maxVotingTimestamp is exceeded + await starknet.devnet.increaseTime(10); + await increaseEthBlockchainTime(10); + + // Execute + await account.invoke( + space, + 'execute', + CallData.compile({ + proposalId: { low: '0x1', high: '0x0' }, + executionPayload: executionPayload, + }), + { rawInput: true }, + ); + // Propogating message to L1 const flushL2Response = await starknet.devnet.flush(); const message_payload = flushL2Response.consumed_messages.from_l2[0].payload; @@ -342,7 +691,7 @@ describe('L1 Avatar Execution', function () { high: message_payload[16], }); - // Couldnt figure out how to handle custom revert message. Manually checked this failed for the correct reason though. + // For some reason CI fails with revertedWith('InvalidProposalStatus') but works locally. await expect( l1AvatarExecutionStrategy.execute( space_message, @@ -388,10 +737,10 @@ describe('L1 Avatar Execution', function () { ]; // Propose - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_propose', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_propose', + CallData.compile({ space: space.address, author: account.address, executionStrategy: { @@ -401,13 +750,14 @@ describe('L1 Avatar Execution', function () { userProposalValidationParams: [], metadataURI: [], }), - }); + { rawInput: true }, + ); // Vote - await account.execute({ - contractAddress: starkTxAuthenticator.address, - entrypoint: 'authenticate_vote', - calldata: CallData.compile({ + await account.invoke( + starkTxAuthenticator, + 'authenticate_vote', + CallData.compile({ space: space.address, voter: account.address, proposalId: { low: '0x1', high: '0x0' }, @@ -415,18 +765,20 @@ describe('L1 Avatar Execution', function () { userVotingStrategies: [{ index: '0x0', params: [] }], metadataURI: [], }), - }); + { rawInput: true }, + ); try { // Execute before maxVotingTimestamp is exceeded - await account.execute({ - contractAddress: space.address, - entrypoint: 'execute', - calldata: CallData.compile({ + await account.invoke( + space, + 'execute', + CallData.compile({ proposalId: { low: '0x1', high: '0x0' }, executionPayload: executionPayload, }), - }); + { rawInput: true }, + ); } catch (err: any) { // 'Before max end timestamp' error message expect(err.message).to.contain('0x4265666f7265206d617820656e642074696d657374616d70'); diff --git a/starknet/test/utils.ts b/starknet/test/utils.ts index e4b68326..7eccfad8 100644 --- a/starknet/test/utils.ts +++ b/starknet/test/utils.ts @@ -20,7 +20,6 @@ export async function safeWithL1AvatarExecutionStrategySetup( const template = await factory.callStatic.createProxyWithNonce(singleton.address, '0x', 0); await factory.createProxyWithNonce(singleton.address, '0x', 0); - console.log('factory deployed'); const safe = GnosisSafeL2.attach(template); await safe.setup( diff --git a/yarn.lock b/yarn.lock index c3d35f84..9ed80f16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2583,6 +2583,15 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" + integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.6.2.tgz#cada0a59e07f5acaeb11cbae7e3ba92aec9c1126" @@ -2982,7 +2991,7 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3212,20 +3221,20 @@ concat-stream@^1.6.0, concat-stream@^1.6.2, concat-stream@~1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.1.tgz#bcab9cacc38c23c503839583151e0fa96fd5b584" - integrity sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ== +concurrently@^7.0.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.6.0.tgz#531a6f5f30cf616f355a4afb8f8fcb2bba65a49a" + integrity sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw== dependencies: - chalk "^4.1.2" - date-fns "^2.30.0" + chalk "^4.1.0" + date-fns "^2.29.1" lodash "^4.17.21" - rxjs "^7.8.1" - shell-quote "^1.8.1" - spawn-command "0.0.2" - supports-color "^8.1.1" + rxjs "^7.0.0" + shell-quote "^1.7.3" + spawn-command "^0.0.2-1" + supports-color "^8.1.0" tree-kill "^1.2.2" - yargs "^17.7.2" + yargs "^17.3.1" convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" @@ -3311,7 +3320,7 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.30.0: +date-fns@^2.29.1: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== @@ -7011,7 +7020,7 @@ rustbn.js@~0.2.0: resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== -rxjs@^7.8.0, rxjs@^7.8.1: +rxjs@^7.0.0, rxjs@^7.8.0: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -7153,7 +7162,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.8.1: +shell-quote@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== @@ -7254,10 +7263,10 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -spawn-command@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" - integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg== split-ca@^1.0.0: version "1.0.1" @@ -7501,7 +7510,7 @@ supports-color@6.0.0: dependencies: has-flag "^3.0.0" -supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: +supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==