Skip to content

Omnichain Governance #283

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

Open
wants to merge 3 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions contracts/governance/omnichain/Executor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
pragma solidity ^0.8.0;

import "../PausableGuardian_0_8.sol";

contract Executor is PausableGuardian_0_8 {
struct TxnData {
address to;
bytes data;
uint256 etherSendAmount;
}

function executeMessage(bytes calldata message) external payable onlyGuardian {
TxnData[] memory transactions = abi.decode(message, (TxnData[]));
uint256 unspentBalance = msg.value;
for (uint i; i < transactions.length;) {
if (transactions[i].etherSendAmount > unspentBalance) {
revert("insufficient funding");
}
unspentBalance -= transactions[i].etherSendAmount;
(bool success, ) = transactions[i].to.call{value:transactions[i].etherSendAmount}(transactions[i].data);
require(success, "fail");
unchecked { ++i; }
}
}
}
37 changes: 37 additions & 0 deletions contracts/governance/omnichain/TimelockMessageDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
pragma solidity ^0.8.0;

import "../../utils/MessageSenderLib.sol";
import "@celer/contracts/message/interfaces/IMessageBus.sol";
import "../../governance/PausableGuardian_0_8.sol";

contract TimelockMessageDistributor is PausableGuardian_0_8 {
mapping(uint64 => address) public chainIdToDest;

IMessageBus public messageBus;

event SetMessageBus(address newMessageBus);

event SendMessage(uint64 indexed destChainId, address indexed destAddress, bytes message);

event SetDestinationForChainId(uint64 indexed destChainId, address destination);

function setMessageBus(IMessageBus msgBus) external onlyGuardian {
messageBus = msgBus;
emit SetMessageBus(address(messageBus));
}

function setDestForID(uint64 chainId, address destination) external onlyGuardian {
chainIdToDest[chainId] = destination;
emit SetDestinationForChainId(chainId, destination);
}

function sendMessageToChain(uint64 chainId, bytes memory message) external payable onlyGuardian {
address destAddress = chainIdToDest[chainId];
MessageSenderLib.sendMessage(destAddress, chainId, message, address(messageBus), computeFee(message));
emit SendMessage(chainId, destAddress, message);
}

function computeFee(bytes memory message) public view returns (uint256) {
return messageBus.calcFee(message);
}
}
89 changes: 89 additions & 0 deletions contracts/governance/omnichain/TimelockReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
pragma solidity ^0.8.0;

import "../PausableGuardian_0_8.sol";
import "../../interfaces/IExecutor.sol";
contract TimelockReceiver is PausableGuardian_0_8 {
address public messageBus;

address public timelockDistributor;

address public executor;

enum ExecutionStatus {
Success,
Fail,
Retry
}

event SetMessageBus(address newMessageBus);

event SetTimeLockDistributor(address newTimeLockDistributor);

event SetExecutor(address newExecutor);

event MessageExecuted(
address indexed executor,
bytes message,
uint256 timestamp
);

event MessageFailed(
address indexed executor,
bytes message,
uint256 timestamp
);

event MessageRetryable(
address indexed executor,
bytes message,
uint256 timestamp
);

function setMessageBus(address msgBus) public onlyGuardian {
messageBus = msgBus;

emit SetMessageBus(msgBus);
}

function setTimelockDistributor(address distributor) public onlyGuardian {
timelockDistributor = distributor;

emit SetTimeLockDistributor(distributor);
}

function setExecutor(address exec) public onlyGuardian {
executor = exec;

emit SetExecutor(exec);
}

modifier onlyMessageBus() {
require(msg.sender == messageBus, "unauthorized");_;
}

function executeMessage(
address sender,
uint64 srcChainId,
bytes calldata message,
address exec
) external payable onlyMessageBus returns (ExecutionStatus) {
if (sender != timelockDistributor || srcChainId != 1) {
return ExecutionStatus.Fail;
}
try IExecutor(executor).executeMessage{value: msg.value}(message) {
emit MessageExecuted(exec, message, block.timestamp);
return ExecutionStatus.Success;
} catch Error(string memory reason) {
if (keccak256(bytes(reason)) == keccak256(bytes("insufficient funding"))) {
emit MessageRetryable(exec, message, block.timestamp);
return ExecutionStatus.Retry;
}
emit MessageFailed(exec, message, block.timestamp);
return ExecutionStatus.Fail;
}catch {
emit MessageFailed(exec, message, block.timestamp);
return ExecutionStatus.Fail;
}
}

}
5 changes: 5 additions & 0 deletions contracts/interfaces/IExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma solidity >=0.5.0 <0.9.0;

interface IExecutor {
function executeMessage(bytes calldata message) external payable;
}
192 changes: 192 additions & 0 deletions contracts/utils/MessageSenderLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
pragma solidity ^0.8.0;

import "@openzeppelin-4.3.2/token/ERC20/IERC20.sol";
import "@openzeppelin-4.3.2/token/ERC20/utils/SafeERC20.sol";
import "@celer/contracts/interfaces/IBridge.sol";
import "@celer/contracts/interfaces/IOriginalTokenVault.sol";
import "@celer/contracts/interfaces/IOriginalTokenVaultV2.sol";
import "@celer/contracts/interfaces/IPeggedTokenBridge.sol";
import "@celer/contracts/interfaces/IPeggedTokenBridgeV2.sol";
import "@celer/contracts/message/interfaces/IMessageBus.sol";
import "@celer/contracts/message/libraries/MsgDataTypes.sol";

library MessageSenderLib {
using SafeERC20 for IERC20;

// ============== Internal library functions called by apps ==============

/**
* @notice Sends a message to an app on another chain via MessageBus without an associated transfer.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* @param _messageBus The address of the MessageBus on this chain.
* @param _fee The fee amount to pay to MessageBus.
*/
function sendMessage(
address _receiver,
uint64 _dstChainId,
bytes memory _message,
address _messageBus,
uint256 _fee
) internal {
IMessageBus(_messageBus).sendMessage{value: _fee}(_receiver, _dstChainId, _message);
}

// Send message to non-evm chain with bytes for receiver address,
// otherwise same as above.
function sendMessage(
bytes calldata _receiver,
uint64 _dstChainId,
bytes memory _message,
address _messageBus,
uint256 _fee
) internal {
IMessageBus(_messageBus).sendMessage{value: _fee}(_receiver, _dstChainId, _message);
}

/**
* @notice Sends a message to an app on another chain via MessageBus with an associated transfer.
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded. Only applicable to the {MsgDataTypes.BridgeSendType.Liquidity}.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* @param _bridgeSendType One of the {MsgDataTypes.BridgeSendType} enum.
* @param _messageBus The address of the MessageBus on this chain.
* @param _fee The fee amount to pay to MessageBus.
* @return The transfer ID.
*/
function sendMessageWithTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
bytes memory _message,
MsgDataTypes.BridgeSendType _bridgeSendType,
address _messageBus,
uint256 _fee
) internal returns (bytes32) {
(bytes32 transferId, address bridge) = sendTokenTransfer(
_receiver,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_bridgeSendType,
_messageBus
);
if (_message.length > 0) {
IMessageBus(_messageBus).sendMessageWithTransfer{value: _fee}(
_receiver,
_dstChainId,
bridge,
transferId,
_message
);
}
return transferId;
}

/**
* @notice Sends a token transfer via a bridge.
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded.
* @param _bridgeSendType One of the {MsgDataTypes.BridgeSendType} enum.
*/
function sendTokenTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
MsgDataTypes.BridgeSendType _bridgeSendType,
address _messageBus
) internal returns (bytes32 transferId, address bridge) {
if (_bridgeSendType == MsgDataTypes.BridgeSendType.Liquidity) {
bridge = IMessageBus(_messageBus).liquidityBridge();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IBridge(bridge).send(_receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage);
transferId = computeLiqBridgeTransferId(_receiver, _token, _amount, _dstChainId, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegDeposit) {
bridge = IMessageBus(_messageBus).pegVault();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IOriginalTokenVault(bridge).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
transferId = computePegV1DepositId(_receiver, _token, _amount, _dstChainId, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegBurn) {
bridge = IMessageBus(_messageBus).pegBridge();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IPeggedTokenBridge(bridge).burn(_token, _amount, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
transferId = computePegV1BurnId(_receiver, _token, _amount, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2Deposit) {
bridge = IMessageBus(_messageBus).pegVaultV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IOriginalTokenVaultV2(bridge).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2Burn) {
bridge = IMessageBus(_messageBus).pegBridgeV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IPeggedTokenBridgeV2(bridge).burn(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2BurnFrom) {
bridge = IMessageBus(_messageBus).pegBridgeV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IPeggedTokenBridgeV2(bridge).burnFrom(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
} else {
revert("bridge type not supported");
}
}

function computeLiqBridgeTransferId(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce
) internal view returns (bytes32) {
return
keccak256(
abi.encodePacked(address(this), _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid))
);
}

function computePegV1DepositId(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce
) internal view returns (bytes32) {
return
keccak256(
abi.encodePacked(address(this), _token, _amount, _dstChainId, _receiver, _nonce, uint64(block.chainid))
);
}

function computePegV1BurnId(
address _receiver,
address _token,
uint256 _amount,
uint64 _nonce
) internal view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), _token, _amount, _receiver, _nonce, uint64(block.chainid)));
}
}
20 changes: 20 additions & 0 deletions testsmainnet/omnichain-governance/timelock_distributor_ethereum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from brownie import *
import pytest

@pytest.fixture(scope="module")
def TIMELOCKMESSAGEDISTRIBUTOR(TimelockMessageDistributor, accounts):
return TimelockMessageDistributor.deploy({"from":accounts[0]})

@pytest.fixture(scope="module")
def MESSAGEBUS():
return "0x4066D196A423b2b3B8B054f4F40efB47a74E200C"

def test_case(TIMELOCKMESSAGEDISTRIBUTOR, MESSAGEBUS):
tLockMsg = TIMELOCKMESSAGEDISTRIBUTOR
tLockMsg.setMessageBus(MESSAGEBUS, {"from":tLockMsg.owner()})
tLockMsg.setDestForID(137, "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", {"from":tLockMsg.owner()}) #random dest address just for testing
message = "Testing Message"
message = message.encode("utf-8")
getCost = tLockMsg.computeFee(message)
tLockMsg.sendMessageToChain(137, message, {"value":getCost, "from":tLockMsg.owner()})
assert(tLockMsg.chainIdToDest(137) == "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174")
Loading