Skip to content

Commit

Permalink
feat: add gnosis implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
romanagureev committed Jun 18, 2024
1 parent 00be3d9 commit 514cd46
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 0 deletions.
139 changes: 139 additions & 0 deletions contracts/gnosis/GnosisBroadcaster.vy
Original file line number Diff line number Diff line change
@@ -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)
61 changes: 61 additions & 0 deletions contracts/gnosis/GnosisRelayer.vy
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions contracts/gnosis/mocks/MockGnosisBridge.vy
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions scripts/gnosis.py
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions tests/gnosis/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
62 changes: 62 additions & 0 deletions tests/gnosis/test_broadcaster.py
Original file line number Diff line number Diff line change
@@ -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())
34 changes: 34 additions & 0 deletions tests/gnosis/test_relayer.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 514cd46

Please sign in to comment.