From bc043a9f679837fbe766018c9358d17a4f4c812d Mon Sep 17 00:00:00 2001 From: Drypto13 Date: Tue, 30 Aug 2022 22:40:57 -0700 Subject: [PATCH 1/3] omni chain governance w/ test cases --- contracts/governance/omnichain/Executor.sol | 31 +++ .../omnichain/TimelockMessageDistributor.sol | 37 ++++ .../governance/omnichain/TimelockReceiver.sol | 73 +++++++ contracts/interfaces/IExecutor.sol | 5 + contracts/utils/MessageSenderLib.sol | 192 ++++++++++++++++++ .../timelock_distributor_ethereum.py | 20 ++ .../governance_polygon.py | 38 ++++ 7 files changed, 396 insertions(+) create mode 100644 contracts/governance/omnichain/Executor.sol create mode 100644 contracts/governance/omnichain/TimelockMessageDistributor.sol create mode 100644 contracts/governance/omnichain/TimelockReceiver.sol create mode 100644 contracts/interfaces/IExecutor.sol create mode 100644 contracts/utils/MessageSenderLib.sol create mode 100644 testsmainnet/omnichain-governance/timelock_distributor_ethereum.py create mode 100644 testspolygon/omnichain-governance/governance_polygon.py diff --git a/contracts/governance/omnichain/Executor.sol b/contracts/governance/omnichain/Executor.sol new file mode 100644 index 00000000..214ba1f8 --- /dev/null +++ b/contracts/governance/omnichain/Executor.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.8.0; + +import "../PausableGuardian_0_8.sol"; + +contract Executor is PausableGuardian_0_8 { + struct TxnData { + address to; + bytes data; + uint256 etherSendAmount; + } + + event MessageExecute( + bytes message, + uint256 timestamp + ); + 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; } + } + + emit MessageExecute(message, block.timestamp); + } +} \ No newline at end of file diff --git a/contracts/governance/omnichain/TimelockMessageDistributor.sol b/contracts/governance/omnichain/TimelockMessageDistributor.sol new file mode 100644 index 00000000..6c05c693 --- /dev/null +++ b/contracts/governance/omnichain/TimelockMessageDistributor.sol @@ -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); + } +} \ No newline at end of file diff --git a/contracts/governance/omnichain/TimelockReceiver.sol b/contracts/governance/omnichain/TimelockReceiver.sol new file mode 100644 index 00000000..7f95b3f9 --- /dev/null +++ b/contracts/governance/omnichain/TimelockReceiver.sol @@ -0,0 +1,73 @@ +pragma solidity ^0.8.0; + +import "../PausableGuardian_0_8.sol"; +import "../../interfaces/IExecutor.sol"; +contract TimelockReceiver is PausableGuardian_0_8 { + enum ExecutionStatus { + Success, + Fail, + Retry + } + + address public messageBus; + + address public timelockDistributor; + + address public executor; + + event SetMessageBus(address newMessageBus); + + event SetTimeLockDistributor(address newTimeLockDistributor); + + event SetExecutor(address newExecutor); + + event MessageToBeExecuted( + 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) { + return ExecutionStatus.Success; + } catch Error(string memory reason) { + if (keccak256(bytes(reason)) == keccak256(bytes("insufficient funding"))) return ExecutionStatus.Retry; + return ExecutionStatus.Fail; + }catch { + return ExecutionStatus.Fail; + } + + emit MessageToBeExecuted(exec, message, block.timestamp); + } + +} \ No newline at end of file diff --git a/contracts/interfaces/IExecutor.sol b/contracts/interfaces/IExecutor.sol new file mode 100644 index 00000000..bde4ee2f --- /dev/null +++ b/contracts/interfaces/IExecutor.sol @@ -0,0 +1,5 @@ +pragma solidity >=0.5.0 <0.9.0; + +interface IExecutor { + function executeMessage(bytes calldata message) external payable; +} \ No newline at end of file diff --git a/contracts/utils/MessageSenderLib.sol b/contracts/utils/MessageSenderLib.sol new file mode 100644 index 00000000..d4390205 --- /dev/null +++ b/contracts/utils/MessageSenderLib.sol @@ -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))); + } +} \ No newline at end of file diff --git a/testsmainnet/omnichain-governance/timelock_distributor_ethereum.py b/testsmainnet/omnichain-governance/timelock_distributor_ethereum.py new file mode 100644 index 00000000..16ec86b1 --- /dev/null +++ b/testsmainnet/omnichain-governance/timelock_distributor_ethereum.py @@ -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") \ No newline at end of file diff --git a/testspolygon/omnichain-governance/governance_polygon.py b/testspolygon/omnichain-governance/governance_polygon.py new file mode 100644 index 00000000..33813830 --- /dev/null +++ b/testspolygon/omnichain-governance/governance_polygon.py @@ -0,0 +1,38 @@ +from distutils.sysconfig import EXEC_PREFIX +from brownie import * +import pytest +from eth_abi import encode_abi + +@pytest.fixture(scope="module") +def TIMELOCKRECEIVER(TimelockReceiver, accounts): + return TimelockReceiver.deploy({"from":accounts[0]}) + +@pytest.fixture(scope="module") +def EXECUTOR(Executor, accounts): + return Executor.deploy({"from":accounts[0]}) + +@pytest.fixture(scope="module") +def BZX(interface): + return interface.IBZx("0x059D60a9CEfBc70b9Ea9FFBb9a041581B1dFA6a8") + +@pytest.fixture(scope="module") +def MESSAGEBUS(): + return "0xaFDb9C40C7144022811F034EE07Ce2E110093fe6" + +@pytest.fixture(scope="module") +def MULTISIG(): + return "0x01F569df8A270eCA78597aFe97D30c65D8a8ca80" + +def test_case(accounts, TIMELOCKRECEIVER, EXECUTOR, BZX, MESSAGEBUS, MULTISIG): + TIMELOCKRECEIVER.setMessageBus(MESSAGEBUS, {"from":accounts[0]}) + TIMELOCKRECEIVER.setTimelockDistributor(accounts[0], {"from":accounts[0]}) + TIMELOCKRECEIVER.setExecutor(EXECUTOR, {"from":accounts[0]}) + TIMELOCKRECEIVER.transferOwnership(MULTISIG, {"from":accounts[0]}) + BZX.transferOwnership(EXECUTOR, {"from":BZX.owner()}) + EXECUTOR.transferOwnership(TIMELOCKRECEIVER, {"from":EXECUTOR.owner()}) + print(BZX.setFeesController.encode_input(MULTISIG)) + print(BZX.setFeesController.encode_input(MULTISIG)[2:]) + txns = [(BZX.address,bytes.fromhex(BZX.setFeesController.encode_input(MULTISIG)[2:]),0)] #test txn on owner only functionality + data = encode_abi(['(address,bytes,uint256)[]'],[txns]) + print(TIMELOCKRECEIVER.executeMessage(accounts[0], 1, data, accounts[0], {"from":MESSAGEBUS}).return_value) + assert(BZX.feesController() == MULTISIG) From f8e0f20dad0dfc87b8266aa44054177c26441fa9 Mon Sep 17 00:00:00 2001 From: Drypto | OOKI <51417606+Drypto13@users.noreply.github.com> Date: Tue, 30 Aug 2022 23:17:03 -0700 Subject: [PATCH 2/3] Update TimelockReceiver.sol --- contracts/governance/omnichain/TimelockReceiver.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/governance/omnichain/TimelockReceiver.sol b/contracts/governance/omnichain/TimelockReceiver.sol index 7f95b3f9..c62ace12 100644 --- a/contracts/governance/omnichain/TimelockReceiver.sol +++ b/contracts/governance/omnichain/TimelockReceiver.sol @@ -55,6 +55,7 @@ contract TimelockReceiver is PausableGuardian_0_8 { bytes calldata message, address exec ) external payable onlyMessageBus returns (ExecutionStatus) { + emit MessageToBeExecuted(exec, message, block.timestamp); if (sender != timelockDistributor || srcChainId != 1) { return ExecutionStatus.Fail; } @@ -66,8 +67,6 @@ contract TimelockReceiver is PausableGuardian_0_8 { }catch { return ExecutionStatus.Fail; } - - emit MessageToBeExecuted(exec, message, block.timestamp); } -} \ No newline at end of file +} From 750634e2ee7684a2c2c68c8ef6e0e689358e5ae4 Mon Sep 17 00:00:00 2001 From: Drypto13 Date: Wed, 31 Aug 2022 12:57:40 -0700 Subject: [PATCH 3/3] additional events and formatting --- contracts/governance/omnichain/Executor.sol | 6 --- .../governance/omnichain/TimelockReceiver.sol | 37 ++++++++++++++----- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/contracts/governance/omnichain/Executor.sol b/contracts/governance/omnichain/Executor.sol index 214ba1f8..53efc577 100644 --- a/contracts/governance/omnichain/Executor.sol +++ b/contracts/governance/omnichain/Executor.sol @@ -9,10 +9,6 @@ contract Executor is PausableGuardian_0_8 { uint256 etherSendAmount; } - event MessageExecute( - bytes message, - uint256 timestamp - ); function executeMessage(bytes calldata message) external payable onlyGuardian { TxnData[] memory transactions = abi.decode(message, (TxnData[])); uint256 unspentBalance = msg.value; @@ -25,7 +21,5 @@ contract Executor is PausableGuardian_0_8 { require(success, "fail"); unchecked { ++i; } } - - emit MessageExecute(message, block.timestamp); } } \ No newline at end of file diff --git a/contracts/governance/omnichain/TimelockReceiver.sol b/contracts/governance/omnichain/TimelockReceiver.sol index c62ace12..36d50a6c 100644 --- a/contracts/governance/omnichain/TimelockReceiver.sol +++ b/contracts/governance/omnichain/TimelockReceiver.sol @@ -3,25 +3,37 @@ pragma solidity ^0.8.0; import "../PausableGuardian_0_8.sol"; import "../../interfaces/IExecutor.sol"; contract TimelockReceiver is PausableGuardian_0_8 { - enum ExecutionStatus { - Success, - Fail, - Retry - } - 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 MessageToBeExecuted( + 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 @@ -55,18 +67,23 @@ contract TimelockReceiver is PausableGuardian_0_8 { bytes calldata message, address exec ) external payable onlyMessageBus returns (ExecutionStatus) { - emit MessageToBeExecuted(exec, message, block.timestamp); 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"))) return ExecutionStatus.Retry; + 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; } } -} +} \ No newline at end of file