diff --git a/README.md b/README.md index 730c638..b8ef3f3 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,22 @@ preferable to generate a fresh account and use it for steps 2 and 3. | [Agent Blueprint](scripts/deploy_agent.py) | [0x0199429171bce183048dccf1d5546ca519ea9717](https://www.okx.com/explorer/xlayer/address/0x0199429171bce183048dccf1d5546ca519ea9717) | +## Gnosis (xdai) + +### Mainnet Deployment Addresses + +| Name | Address | +|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| [L1 Broadcaster](contracts/gnosis/GnosisBroadcaster.vy) | [0x22089A449ABdAd415d3B8476A501BFe70870C1a7](https://eth.blockscout.com/address/0x22089A449ABdAd415d3B8476A501BFe70870C1a7) | +| [L2 Relayer](contracts/gnosis/GnosisRelayer.vy) | [0x22089A449ABdAd415d3B8476A501BFe70870C1a7](https://gnosis.blockscout.com/address/0x22089A449ABdAd415d3B8476A501BFe70870C1a7) | +| [L2 Ownership Agent](contracts/Agent.vy) | [0x383544581A70d2C4E4688d2C5C18C3941e0c8637](https://gnosis.blockscout.com/address/0x383544581A70d2C4E4688d2C5C18C3941e0c8637) | +| [L2 Parameter Agent](contracts/Agent.vy) | [0x91304259119506185Fd74e3950bdd65A7e03E15E](https://gnosis.blockscout.com/address/0x91304259119506185Fd74e3950bdd65A7e03E15E) | +| [L2 Emergency Agent](contracts/Agent.vy) | [0xEFDA01FE1dE71c9bDcFd78A58EA34d9F8f8bde90](https://gnosis.blockscout.com/address/0xEFDA01FE1dE71c9bDcFd78A58EA34d9F8f8bde90) | +| [L2 Vault](contracts/Vault.vy) | [0x0B8c6A25904a1b8A0712Bc857390130a438c52AA](https://gnosis.blockscout.com/address/0x0B8c6A25904a1b8A0712Bc857390130a438c52AA) | +| [Agent Blueprint](scripts/deploy_agent.py) | [0x61951AC5664c7a7d7aB7df9892a82a5fCd622Bb2](https://gnosis.blockscout.com/address/0x61951AC5664c7a7d7aB7df9892a82a5fCd622Bb2) | + + + ## Base ### Mainnet Deployment Addresses diff --git a/contracts/gnosis/GnosisBroadcaster.vy b/contracts/gnosis/GnosisBroadcaster.vy new file mode 100644 index 0000000..e36b3c3 --- /dev/null +++ b/contracts/gnosis/GnosisBroadcaster.vy @@ -0,0 +1,139 @@ +# @version 0.3.10 +""" +@title Gnosis Broadcaster +@author CurveFi +@notice Using Arbitrary Message Bridge (AMB) +""" + + +interface ArbitraryMessageBridge: + def requireToPassMessage(_contract: address, _data: Bytes[(MAX_BYTES + 160) * MAX_MESSAGES], _gas: uint256) -> bytes32: nonpayable + def maxGasPerTx() -> uint256: view + + +event ApplyAdmins: + admins: AdminSet + +event CommitAdmins: + future_admins: AdminSet + +event SetBridge: + bridge: ArbitraryMessageBridge + + +enum Agent: + OWNERSHIP + PARAMETER + EMERGENCY + + +struct AdminSet: + ownership: address + parameter: address + emergency: address + +struct Message: + target: address + data: Bytes[MAX_BYTES] + + +MAX_BYTES: constant(uint256) = 1024 +MAX_MESSAGES: constant(uint256) = 8 + +admins: public(AdminSet) +future_admins: public(AdminSet) + +agent: HashMap[address, Agent] + +bridge: public(ArbitraryMessageBridge) + + +@external +def __init__(_admins: AdminSet, _bridge: ArbitraryMessageBridge): + assert _admins.ownership != _admins.parameter # a != b + assert _admins.ownership != _admins.emergency # a != c + assert _admins.parameter != _admins.emergency # b != c + + self.admins = _admins + + self.agent[_admins.ownership] = Agent.OWNERSHIP + self.agent[_admins.parameter] = Agent.PARAMETER + self.agent[_admins.emergency] = Agent.EMERGENCY + + self.bridge = _bridge + + log ApplyAdmins(_admins) + log SetBridge(_bridge) + + +@external +def broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint256=0): + """ + @notice Broadcast a sequence of messages. + @param _messages The sequence of messages to broadcast. + @param _gas_limit The L2 gas limit required to execute the sequence of messages. + """ + agent: Agent = self.agent[msg.sender] + assert agent != empty(Agent) + + bridge: ArbitraryMessageBridge = self.bridge + gas_limit: uint256 = _gas_limit if _gas_limit > 0 else bridge.maxGasPerTx() + + bridge.requireToPassMessage( + self, + _abi_encode( # relay(uint256,(address,bytes)[]) + agent, + _messages, + method_id=method_id("relay(uint256,(address,bytes)[])"), + ), + gas_limit, + ) + + +@external +def set_bridge(_bridge: ArbitraryMessageBridge): + """ + @notice Set ArbitraryMessageBridge contract proxy. + """ + assert msg.sender == self.admins.ownership + + self.bridge = _bridge + log SetBridge(_bridge) + + +@external +def commit_admins(_future_admins: AdminSet): + """ + @notice Commit an admin set to use in the future. + """ + assert msg.sender == self.admins.ownership + + assert _future_admins.ownership != _future_admins.parameter # a != b + assert _future_admins.ownership != _future_admins.emergency # a != c + assert _future_admins.parameter != _future_admins.emergency # b != c + + self.future_admins = _future_admins + log CommitAdmins(_future_admins) + + +@external +def apply_admins(): + """ + @notice Apply the future admin set. + """ + admins: AdminSet = self.admins + assert msg.sender == admins.ownership + + # reset old admins + self.agent[admins.ownership] = empty(Agent) + self.agent[admins.parameter] = empty(Agent) + self.agent[admins.emergency] = empty(Agent) + + # set new admins + future_admins: AdminSet = self.future_admins + self.agent[future_admins.ownership] = Agent.OWNERSHIP + self.agent[future_admins.parameter] = Agent.PARAMETER + self.agent[future_admins.emergency] = Agent.EMERGENCY + + self.admins = future_admins + log ApplyAdmins(future_admins) diff --git a/contracts/gnosis/GnosisRelayer.vy b/contracts/gnosis/GnosisRelayer.vy new file mode 100644 index 0000000..4a8d552 --- /dev/null +++ b/contracts/gnosis/GnosisRelayer.vy @@ -0,0 +1,61 @@ +# @version 0.3.10 +""" +@title Gnosis Relayer +@author CurveFi +""" + + +interface IAgent: + def execute(_messages: DynArray[Message, MAX_MESSAGES]): nonpayable + + +enum Agent: + OWNERSHIP + PARAMETER + EMERGENCY + + +struct Message: + target: address + data: Bytes[MAX_BYTES] + + +MAX_BYTES: constant(uint256) = 1024 +MAX_MESSAGES: constant(uint256) = 8 + +CODE_OFFSET: constant(uint256) = 3 + + +MESSENGER: public(immutable(address)) + +OWNERSHIP_AGENT: public(immutable(address)) +PARAMETER_AGENT: public(immutable(address)) +EMERGENCY_AGENT: public(immutable(address)) + + +agent: HashMap[Agent, address] + + +@external +def __init__(_agent_blueprint: address, _messenger: address): + MESSENGER = _messenger + + OWNERSHIP_AGENT = create_from_blueprint(_agent_blueprint, code_offset=CODE_OFFSET) + PARAMETER_AGENT = create_from_blueprint(_agent_blueprint, code_offset=CODE_OFFSET) + EMERGENCY_AGENT = create_from_blueprint(_agent_blueprint, code_offset=CODE_OFFSET) + + self.agent[Agent.OWNERSHIP] = OWNERSHIP_AGENT + self.agent[Agent.PARAMETER] = PARAMETER_AGENT + self.agent[Agent.EMERGENCY] = EMERGENCY_AGENT + + +@external +def relay(_agent: Agent, _messages: DynArray[Message, MAX_MESSAGES]): + """ + @notice Receive messages for an agent and relay them. + @param _agent The agent to relay messages to. + @param _messages The sequence of messages to relay. + """ + assert msg.sender == MESSENGER + + IAgent(self.agent[_agent]).execute(_messages) diff --git a/contracts/gnosis/mocks/MockGnosisBridge.vy b/contracts/gnosis/mocks/MockGnosisBridge.vy new file mode 100644 index 0000000..6391295 --- /dev/null +++ b/contracts/gnosis/mocks/MockGnosisBridge.vy @@ -0,0 +1,25 @@ +# pragma version 0.3.10 + + +MAX_LEN: constant(uint256) = 1024 + +contract: public(address) +data: public(Bytes[MAX_LEN]) +gas: public(uint256) + + +@view +@external +def maxGasPerTx() -> uint256: + return 4_000_000 + + +@external +def requireToPassMessage(_contract: address, _data: Bytes[MAX_LEN], _gas: uint256) -> bytes32: + assert _gas >= 100 + assert _gas <= 4_000_000 + + self.contract = _contract + self.data = _data + self.gas = _gas + return convert(101, bytes32) diff --git a/scripts/gnosis.py b/scripts/gnosis.py new file mode 100644 index 0000000..a205b11 --- /dev/null +++ b/scripts/gnosis.py @@ -0,0 +1,34 @@ +import click +from ape import project +from ape.cli import ConnectedProviderCommand, account_option, network_option + + +@click.command(cls=ConnectedProviderCommand) +@account_option() +@network_option() +@click.option("--blueprint") +def cli(account, network, blueprint): + chain_id = project.provider.chain_id + + if chain_id not in (1,): + relayer = project.GnosisRelayer.deploy( + blueprint, + "0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59", + gas_limit=800_000, + gas_price=project.provider.gas_price, + sender=account, + ) + return project.Vault.deploy( + relayer.OWNERSHIP_AGENT(), gas_price=project.provider.gas_price, sender=account + ) + + # L1 + if chain_id == 1: + admins = ( + "0x40907540d8a6C65c637785e8f8B742ae6b0b9968", + "0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f", + "0x467947EE34aF926cF1DCac093870f613C96B1E0c", + ) + bridge = "0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e" + + return project.GnosisBroadcaster.deploy(admins, bridge, sender=account) diff --git a/tests/gnosis/conftest.py b/tests/gnosis/conftest.py new file mode 100644 index 0000000..eb25fe8 --- /dev/null +++ b/tests/gnosis/conftest.py @@ -0,0 +1,22 @@ +import pytest + + +@pytest.fixture(scope="module") +def mock_bridge(alice, project): + yield project.MockGnosisBridge.deploy(sender=alice) + + +@pytest.fixture(scope="module") +def relayer(alice, project, agent_blueprint, mock_bridge): + relayer = project.GnosisRelayer.deploy(agent_blueprint, mock_bridge, sender=alice) + yield relayer + + +@pytest.fixture(scope="module") +def agents(relayer): + yield [getattr(relayer, attr + "_AGENT")() for attr in ["OWNERSHIP", "PARAMETER", "EMERGENCY"]] + + +@pytest.fixture(scope="module") +def broadcaster(alice, bob, charlie, project, mock_bridge): + yield project.GnosisBroadcaster.deploy((alice, bob, charlie), mock_bridge, sender=alice) diff --git a/tests/gnosis/test_broadcaster.py b/tests/gnosis/test_broadcaster.py new file mode 100644 index 0000000..4c5bf3f --- /dev/null +++ b/tests/gnosis/test_broadcaster.py @@ -0,0 +1,62 @@ +import itertools + +import ape +import eth_abi +import pytest +from eth_utils import keccak + + +def test_constructor(alice, bob, charlie, broadcaster, mock_bridge): + assert broadcaster.admins() == (alice, bob, charlie) + assert broadcaster.bridge() == mock_bridge + + +@pytest.mark.parametrize("idx,gas", itertools.product(range(3), [0, 1_000])) +def test_broadcast_success(alice, bob, charlie, broadcaster, mock_bridge, idx, gas): + msg_sender = [alice, bob, charlie][idx] + + if gas > 0: + broadcaster.broadcast([(alice.address, b"")], gas, sender=msg_sender) + else: + broadcaster.broadcast([(alice.address, b"")], sender=msg_sender) + + decoded = eth_abi.decode(["uint256", "(address,bytes)[]"], mock_bridge.data()[4:]) + + assert len(mock_bridge.data()) < 500 + assert mock_bridge.data()[:4] == keccak(text="relay(uint256,(address,bytes)[])")[:4] + assert decoded[0] == 2**idx + assert decoded[1] == ((alice.address.lower(), b""),) + + assert mock_bridge.gas() == gas if gas > 0 else mock_bridge.maxGasPerTx() + + +def test_broadcast_reverts(dave, broadcaster): + with ape.reverts(): + broadcaster.broadcast([(dave.address, b"")], sender=dave) + + +def test_commit_admins(alice, bob, charlie, broadcaster): + tx = broadcaster.commit_admins((alice, bob, charlie), sender=alice) + + assert broadcaster.future_admins() == (alice, bob, charlie) + assert len(tx.logs) == 1 + assert tx.logs[0]["topics"][0] == keccak("CommitAdmins((address,address,address))".encode()) + + with ape.reverts(): + broadcaster.commit_admins((bob, charlie, alice), sender=bob) + + with ape.reverts(): + broadcaster.commit_admins((alice, alice, alice), sender=alice) + + +def test_apply_admins(alice, bob, charlie, broadcaster): + broadcaster.commit_admins((charlie, bob, alice), sender=alice) + + with ape.reverts(): + broadcaster.apply_admins(sender=bob) + + tx = broadcaster.apply_admins(sender=alice) + + assert broadcaster.admins() == (charlie, bob, alice) + assert len(tx.logs) == 1 + assert tx.logs[0]["topics"][0] == keccak("ApplyAdmins((address,address,address))".encode()) diff --git a/tests/gnosis/test_relayer.py b/tests/gnosis/test_relayer.py new file mode 100644 index 0000000..e5cc6f3 --- /dev/null +++ b/tests/gnosis/test_relayer.py @@ -0,0 +1,34 @@ +import math + +import ape +import pytest + +from tests import AgentEnum + + +def test_constructor(alice, project, agent_blueprint, mock_bridge, ZERO_ADDRESS): + relayer = project.OptimismRelayer.deploy(agent_blueprint, mock_bridge, sender=alice) + + assert relayer.OWNERSHIP_AGENT() != ZERO_ADDRESS + assert relayer.PARAMETER_AGENT() != ZERO_ADDRESS + assert relayer.EMERGENCY_AGENT() != ZERO_ADDRESS + assert relayer.MESSENGER() == mock_bridge + + +@pytest.mark.parametrize("agent", AgentEnum) +def test_relay_success(alice, relayer, mock_bridge, agent, agents): + agent_addr = agents[int(math.log2(agent))] + tx = relayer.relay(agent, [(alice.address, b"")], sender=mock_bridge) + + targets = [f.contract_address for f in tx.trace if f.op == "CALL"] + assert {agent_addr, alice.address} == set(targets) + + +def test_relay_invalid_caller(alice, relayer): + with ape.reverts(): + relayer.relay(AgentEnum.OWNERSHIP, [(alice.address, b"")], sender=alice) + + +def test_relay_invalid_agent(alice, relayer, mock_bridge): + with ape.reverts(): + relayer.relay(42, [(alice.address, b"")], sender=mock_bridge)