Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: l1 execution #516

Merged
merged 44 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e1eb4e5
fix: eth relayer
Aug 22, 2023
33b96f9
refactor: use deserialization for payload
Aug 22, 2023
172745f
fix: l1 avatar payload serialization
Aug 22, 2023
b73dec3
fix: enforce timestamp exceed max voting timestamp
Aug 28, 2023
d23e659
feat: working L1 avatar execution
Aug 28, 2023
72ab36e
chore: mock core contracts and avatar execution for testing
Aug 28, 2023
e0077ed
chore: deps management
Aug 28, 2023
944a551
chore: test/ dir with lq exec test
Aug 28, 2023
813fd0e
chore: more deps managment
Aug 28, 2023
b63ed4d
chore: hardhat config
Aug 28, 2023
082c97c
chore: formatting
Aug 28, 2023
73e3e58
Merge branch 'develop' into fix_l1_exec
Orland0x Aug 28, 2023
4770766
Merge branch 'develop' into fix_l1_exec
Aug 28, 2023
804a88c
chore: L1 Avatar unit forge tests
Aug 29, 2023
8d8454e
chore: deps and cleanup
Aug 29, 2023
cd9cda0
chore: temp remove deps.sol
Aug 29, 2023
afdb9af
fix: relayer imports
Aug 29, 2023
af30b06
chore: foundry deps
Aug 29, 2023
70be85b
chore: fix typo
Aug 29, 2023
4b29fb5
chore: re-add stark sig auth test
Aug 29, 2023
5ca5da2
chore: l1 execution revert tests
Aug 31, 2023
360357a
chore: l1 execution bash script
Aug 31, 2023
9090930
chore: updated deps
Aug 31, 2023
4ab65ec
chore: updated to latest safe versions for setup and pull artifacts f…
Aug 31, 2023
818b037
chore: cleanup test
Aug 31, 2023
f54fcac
chore: updated sol imports
Aug 31, 2023
13ac9ac
Merge branch 'develop' into fix_l1_exec
Aug 31, 2023
1132599
feat: get strategy type on eth relayer
Aug 31, 2023
3a22511
chore: fixe remappings
Aug 31, 2023
d1a3b3e
chore: More test coverage and CI integration
Sep 3, 2023
571ec08
chore: cleanup
Sep 3, 2023
fa6c7d0
chore: PR fixes
Sep 4, 2023
777942d
chore: more coverage and refactor
Sep 4, 2023
95d9631
chore: checks on space manager during init
Sep 4, 2023
498879d
chore: fixed revert string
Sep 4, 2023
1c055a5
chore: update .env.example
Orland0x Sep 4, 2023
eda97bd
Merge branch 'develop' into fix_l1_exec
Sep 4, 2023
091b857
chore: moved safe utils to external sub dir
Sep 4, 2023
a36f69a
chore formatting
Sep 4, 2023
4ce5bce
feat: quorum setter and test
Sep 4, 2023
a34512e
vhore: test invalid space call exec strat
Sep 4, 2023
ddafb71
chore: formatting
Sep 4, 2023
3f2d438
chore: fix revert msg in test
Sep 4, 2023
27f5b1c
feat: setters for Starknet Core and Eth Relayer contracts and tests
Sep 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@
url = https://github.com/safe-global/safe-contracts
[submodule "lib/safe-contracts"]
branch = v1.4.0
[submodule "ethereum/lib/openzeppelin-contracts"]
path = ethereum/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions ethereum/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at fd81a9
5 changes: 4 additions & 1 deletion ethereum/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
@gnosis.pm/safe-contracts=lib/safe-contracts
@gnosis.pm/safe-contracts=lib/safe-contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts
@gnosis.pm/zodiac/=lib/zodiac/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts
9 changes: 9 additions & 0 deletions ethereum/src/deps.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// SPDX-License-Identifier: MIT

// pragma solidity ^0.8.19;

// import "@gnosis.pm/safe-contracts/contracts/SafeL2.sol";
// import "@gnosis.pm/safe-contracts/contracts/proxies/SafeProxyFactory.sol";
// import "@gnosis.pm/zodiac/contracts/factory/ModuleProxyFactory.sol";
// import "@gnosis.pm/zodiac/contracts/guard/Guardable.sol";
// import "@gnosis.pm/zodiac/contracts/core/Module.sol";
130 changes: 65 additions & 65 deletions ethereum/src/execution-strategies/L1AvatarExecutionStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@

pragma solidity ^0.8.19;

import "zodiac/interfaces/IAvatar.sol";
import "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol";
import "../interfaces/IStarknetCore.sol";
import {SimpleQuorumExecutionStrategy} from "./SimpleQuorumExecutionStrategy.sol";
import "../types.sol";
/**
* @title Snapshot X L1 execution Zodiac module
* @author Snapshot Labs
* @notice Trustless L1 execution of Snapshot X decisions via an Avatar contract such as a Gnosis Safe
* @dev Work in progress
*/

/// @title L1 Avatar Execution Strategy
/// @notice Used to execute SX Starknet proposal transactions from an Avatar contract on Ethereum.
/// @dev An Avatar contract is any contract that implements the IAvatar interface, eg a Gnosis Safe.
contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {
/// @dev Address of the avatar that this module will pass transactions to.
address public target;

/// The StarkNet Core contract
/// The Starknet Core contract.
address public starknetCore;

/// Address of the StarkNet contract that will send execution details to this contract in a L2 -> L1 message
Expand All @@ -29,66 +26,65 @@ contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {
/// @dev Emitted each time the Execution Relayer is set.
event ExecutionRelayerSet(uint256 indexed newExecutionRelayer);

/**
* @dev Emitted when a new module proxy instance has been deployed
* @param _owner Address of the owner of this contract
* @param _target Address that this contract will pass transactions to
* @param _starknetCore Address of the StarkNet Core contract
* @param _executionRelayer Address of the StarkNet contract that will send execution details to this contract in a L2 -> L1 message
*/
event SXAvatarExecutorSetUp(
/// @notice Emitted when a new Avatar Execution Strategy is initialized.
/// @param _owner Address of the owner of the strategy.
/// @param _target Address of the avatar that this module will pass transactions to.
/// @param _starknetCore Address of the StarkNet Core contract.
/// @param _executionRelayer Address of the StarkNet contract that will send execution details to this contract in a L2 -> L1 message
/// @param _starknetSpaces Array of whitelisted space contracts.
/// @param _quorum The quorum required to execute a proposal.
event L1AvatarExecutionStrategySetUp(
address indexed _owner,
address _target,
address _starknetCore,
uint256 _executionRelayer,
uint256[] _starknetSpaces
uint256[] _starknetSpaces,
uint256 _quorum
);

/**
* @dev Constructs the master contract
* @param _owner Address of the owner of this contract
* @param _target Address that this contract will pass transactions to
* @param _starknetCore Address of the StarkNet Core contract
* @param _executionRelayer Address of the StarkNet contract that will send execution details to this contract in a L2 -> L1 message
* @param _starknetSpaces of spaces deployed on StarkNet that are allowed to execute proposals via this contract
*/
/// @notice Constructor
/// @param _owner Address of the owner of this contract.
/// @param _target Address of the avatar that this module will pass transactions to.
/// @param _starknetCore Address of the StarkNet Core contract.
/// @param _executionRelayer Address of the StarkNet contract that will send execution details to this contract in a L2 -> L1 message
/// @param _starknetSpaces Array of whitelisted space contracts.
/// @param _quorum The quorum required to execute a proposal.
constructor(
address _owner,
address _target,
address _starknetCore,
uint256 _executionRelayer,
uint256[] memory _starknetSpaces
uint256[] memory _starknetSpaces,
uint256 _quorum
) {
bytes memory initParams = abi.encode(_owner, _target, _starknetCore, _executionRelayer, _starknetSpaces);
bytes memory initParams =
abi.encode(_owner, _target, _starknetCore, _executionRelayer, _starknetSpaces, _quorum);
setUp(initParams);
}

/**
* @dev Proxy constructor
* @param initParams Initialization parameters
*/
/// @notice Initialization function, should be called immediately after deploying a new proxy to this contract.
/// @param initParams ABI encoded parameters, in the same order as the constructor.
function setUp(bytes memory initParams) public initializer {
(
address _owner,
address _target,
address _starknetCore,
uint256 _executionRelayer,
uint256[] memory _starknetSpaces
) = abi.decode(initParams, (address, address, address, uint256, uint256[]));
uint256[] memory _starknetSpaces,
uint256 _quorum
) = abi.decode(initParams, (address, address, address, uint256, uint256[], uint256));
__Ownable_init();
transferOwnership(_owner);
__SpaceManager_init(_starknetSpaces);
__SimpleQuorumExecutionStrategy_init(_quorum);
target = _target;
starknetCore = _starknetCore;
executionRelayer = _executionRelayer;

emit SXAvatarExecutorSetUp(_owner, _target, _starknetCore, _executionRelayer, _starknetSpaces);
emit L1AvatarExecutionStrategySetUp(_owner, _target, _starknetCore, _executionRelayer, _starknetSpaces, _quorum);
}

/**
* @dev Changes the StarkNet execution relayer contract
* @param _executionRelayer Address of the new execution relayer contract
*/
/// @notice Sets the Starknet execution relayer contract
/// @param _executionRelayer Address of the new execution relayer contract
function setExecutionRelayer(uint256 _executionRelayer) external onlyOwner {
executionRelayer = _executionRelayer;
emit ExecutionRelayerSet(_executionRelayer);
Expand All @@ -107,16 +103,16 @@ contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {
/// @param votesFor The number of votes for the proposal.
/// @param votesAgainst The number of votes against the proposal.
/// @param votesAbstain The number of votes abstaining from the proposal.
/// @param executionHash The hash of the execution payload.
/// @param payload The encoded execution payload.
/// @param executionHash The hash of the proposal transactions.
/// @param transactions The proposal transactions to be executed.
function execute(
uint256 space,
Proposal memory proposal,
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain,
bytes32 executionHash,
bytes memory payload
uint256 executionHash,
MetaTransaction[] memory transactions
) external onlySpace(space) {
// Call to the Starknet core contract will fail if finalized proposal message was not received on L1.
_receiveProposal(space, proposal, votesFor, votesAgainst, votesAbstain, executionHash);
Expand All @@ -126,9 +122,9 @@ contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {
revert InvalidProposalStatus(proposalStatus);
}

if (executionHash != keccak256(payload)) revert InvalidPayload();
if (bytes32(executionHash) != keccak256(abi.encode(transactions))) revert InvalidPayload();

_execute(payload);
_execute(transactions);
}

/// @dev Reverts if the expected message was not received from L2.
Expand All @@ -138,36 +134,40 @@ contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain,
bytes32 executionHash
uint256 executionHash
) internal {
uint256[] memory payload = new uint256[](15);
// The Cairo serialization of the payload sent from L2
uint256[] memory payload = new uint256[](19);
payload[0] = space;
// The serialized Proposal struct
// TODO: this is probably an incorrect serialization
payload[1] = uint256(proposal.snapshotTimestamp);
payload[2] = uint256(proposal.startTimestamp);
payload[3] = uint256(proposal.minEndTimestamp);
payload[4] = uint256(proposal.maxEndTimestamp);
payload[1] = uint256(proposal.startTimestamp);
payload[2] = uint256(proposal.minEndTimestamp);
payload[3] = uint256(proposal.maxEndTimestamp);
payload[4] = uint256(proposal.finalizationStatus);
payload[5] = proposal.executionPayloadHash;
payload[6] = uint256(uint160(proposal.executionStrategy));
payload[7] = uint256(uint160(proposal.author));
payload[8] = uint256(proposal.finalizationStatus);
payload[9] = proposal.activeVotingStrategies;
payload[6] = proposal.executionStrategy;
payload[7] = proposal.authorAddressType;
payload[8] = proposal.author;
payload[9] = proposal.activeVotingStrategies & (2 ** 128 - 1);
payload[10] = proposal.activeVotingStrategies >> 128;

payload[11] = votesFor & (2 ** 128 - 1);
payload[12] = votesFor >> 128;

payload[13] = votesAgainst & (2 ** 128 - 1);
payload[14] = votesAgainst >> 128;

payload[10] = votesFor;
payload[11] = votesAgainst;
payload[12] = votesAbstain;
payload[15] = votesAbstain & (2 ** 128 - 1);
payload[16] = votesAbstain >> 128;

payload[13] = uint256(executionHash >> 128); // High 128 bits of executionHash
payload[14] = uint256(executionHash) & (2 ** 128 - 1); // Low 128 bits of executionHash
payload[17] = executionHash & (2 ** 128 - 1);
payload[18] = executionHash >> 128;

// If proposal execution message did not exist/not received yet, then this will revert.
IStarknetCore(starknetCore).consumeMessageFromL2(executionRelayer, payload);
}

/// @dev Decodes and executes the payload via the avatar.
function _execute(bytes memory payload) internal {
MetaTransaction[] memory transactions = abi.decode(payload, (MetaTransaction[]));
function _execute(MetaTransaction[] memory transactions) internal {
for (uint256 i = 0; i < transactions.length; i++) {
bool success = IAvatar(target).execTransactionFromModule(
transactions[i].to, transactions[i].value, transactions[i].data, transactions[i].operation
Expand All @@ -179,6 +179,6 @@ contract L1AvatarExecutionStrategy is SimpleQuorumExecutionStrategy {

/// @notice Returns the type of execution strategy.
function getStrategyType() external pure override returns (string memory) {
return "SimpleQuorumL1AvatarExecutionStrategy";
return "SimpleQuorumL1Avatar";
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;
pragma solidity ^0.8.19;

import {IExecutionStrategy} from "../interfaces/IExecutionStrategy.sol";
import {FinalizationStatus, Proposal, ProposalStatus} from "../types.sol";
import {StarknetSpaceManager} from "./StarknetSpaceManager.sol";

/// @title Simple Quorum Base Execution Strategy
abstract contract SimpleQuorumExecutionStrategy is IExecutionStrategy, StarknetSpaceManager {
event QuorumUpdated(uint256 newQuorum);
Orland0x marked this conversation as resolved.
Show resolved Hide resolved

/// @notice The quorum required to execute a proposal using this strategy.
uint256 public quorum;

Expand Down
31 changes: 16 additions & 15 deletions ethereum/src/execution-strategies/StarknetSpaceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,57 @@
pragma solidity ^0.8.19;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {TRUE, FALSE} from "../types.sol";

/// @title Space Manager - A contract that manages SX spaces on Starknet that are able to execute transactions via this contract
/// @author Snapshot Labs
/// @title Space Manager
/// @notice Manages a whitelist of Spaces that are authorized to execute transactions via this contract.
contract StarknetSpaceManager is OwnableUpgradeable {
/// @notice Thrown if a space is not in the whitelist.
error InvalidSpace();

/// @dev Mapping of spaces that are enabled.
/// A uint256 is used as Starknet addresses cannot be cast to a solidity address type.
mapping(uint256 => bool) internal spaces;
mapping(uint256 spaces => uint256 isEnabled) internal spaces;

/// @notice Emitted when a space is enabled.
event SpaceEnabled(uint256 space);

/// @notice Emitted when a space is disabled.
event SpaceDisabled(uint256 space);

/// @notice Initialize the contract with a list of spaces. Called only once.
/// @notice Initialize the contract with a list of Starknet spaces. Called only once.
/// @param _spaces List of spaces.
function __SpaceManager_init(uint256[] memory _spaces) internal onlyInitializing {
for (uint256 i = 0; i < _spaces.length; i++) {
if (_spaces[i] == 0 || isSpaceEnabled(_spaces[i])) revert InvalidSpace();
Orland0x marked this conversation as resolved.
Show resolved Hide resolved
spaces[_spaces[i]] = true;
spaces[_spaces[i]] = TRUE;
}
}

/// @notice Enable a space.
/// @param space Address of the space.
function enableSpace(uint256 space) public onlyOwner {
if (space == 0 || isSpaceEnabled(space)) revert InvalidSpace();
spaces[space] = true;
if (space == 0 || (spaces[space] != FALSE)) revert InvalidSpace();
spaces[space] = TRUE;
emit SpaceEnabled(space);
}

/// @notice Disable a space.
/// @param space Address of the space.
function disableSpace(uint256 space) public onlyOwner {
if (!spaces[space]) revert InvalidSpace();
spaces[space] = false;
function disableSpace(uint256 space) external onlyOwner {
Orland0x marked this conversation as resolved.
Show resolved Hide resolved
if (spaces[space] == FALSE) revert InvalidSpace();
spaces[space] = FALSE;
emit SpaceDisabled(space);
}

/// @notice Check if a space is enabled.
/// @param space Address of the space.
/// @return bool whether the space is enabled.
function isSpaceEnabled(uint256 space) public view returns (bool) {
/// @return uint256 whether the space is enabled.
function isSpaceEnabled(uint256 space) external view returns (uint256) {
return spaces[space];
}

modifier onlySpace(uint256 callerAddress) {
if (!isSpaceEnabled(callerAddress)) revert InvalidSpace();
modifier onlySpace(uint256 space) {
if (spaces[space] == FALSE) revert InvalidSpace();
_;
}
}
44 changes: 44 additions & 0 deletions ethereum/src/mocks/Avatar.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import {IAvatar} from "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol";

contract Avatar {
error NotAuthorized();

mapping(address module => bool isEnabled) internal modules;

// solhint-disable-next-line no-empty-blocks
receive() external payable {}

function enableModule(address _module) external {
modules[_module] = true;
}

function disableModule(address _module) external {
modules[_module] = false;
}

function isModuleEnabled(address _module) external view returns (bool) {
return modules[_module];
}

function execTransactionFromModule(address payable to, uint256 value, bytes calldata data, uint8 operation)
external
returns (bool success)
{
if (!modules[msg.sender]) revert NotAuthorized();
// solhint-disable-next-line avoid-low-level-calls
if (operation == 1) (success,) = to.delegatecall(data);
else (success,) = to.call{value: value}(data);
}

function getModulesPaginated(
address,
uint256 // pageSize
) external pure returns (address[] memory array, address next) {
// Unimplemented
return (new address[](0), address(0));
}
}
Loading
Loading