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

Transact from sub to eth #1145

Open
wants to merge 25 commits into
base: bridge-next-gen
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {SubstrateTypes} from "./SubstrateTypes.sol";

import {IERC20} from "./interfaces/IERC20.sol";
import {SafeTokenTransfer, SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {Call} from "./utils/Call.sol";

/// @title Code which will run within an `Agent` using `delegatecall`.
/// @dev This is a singleton contract, meaning that all agents will execute the same code.
contract AgentExecutor {
using SafeTokenTransfer for IERC20;
using SafeNativeTransfer for address payable;
using Call for address;

/// @dev Execute a message which originated from the Polkadot side of the bridge. In other terms,
/// the `data` parameter is constructed by the BridgeHub parachain.
Expand All @@ -23,6 +25,10 @@ contract AgentExecutor {
(address token, address recipient, uint128 amount) = abi.decode(params, (address, address, uint128));
_transferToken(token, recipient, amount);
}
if (command == AgentExecuteCommand.Transact) {
(address target, bytes memory payload, uint64 dynamicGas) = abi.decode(params, (address, bytes, uint64));
_executeCall(target, payload, dynamicGas);
}
}

/// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`,
Expand All @@ -36,4 +42,10 @@ contract AgentExecutor {
function _transferToken(address token, address recipient, uint128 amount) internal {
IERC20(token).safeTransfer(recipient, amount);
}

/// @dev Call a contract at the given address, with provided bytes as payload.
function _executeCall(address target, bytes memory payload, uint64 dynamicGas) internal returns (bytes memory) {
(bool success, bytes memory data) = target.excessivelySafeCall(dynamicGas, 0, 256, payload);
return Call.verifyResult(success, data);
}
}
2 changes: 2 additions & 0 deletions contracts/src/DeployScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {ChannelID, ParaID, OperatingMode} from "./Types.sol";
import {SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {stdJson} from "forge-std/StdJson.sol";
import {UD60x18, ud60x18} from "prb/math/src/UD60x18.sol";
import {HelloWorld} from "../test/mocks/HelloWorld.sol";

contract DeployScript is Script {
using SafeNativeTransfer for address payable;
Expand Down Expand Up @@ -99,6 +100,7 @@ contract DeployScript is Script {
payable(assetHubAgent).safeNativeTransfer(initialDeposit);

new GatewayUpgradeMock();
new HelloWorld();
yrong marked this conversation as resolved.
Show resolved Hide resolved

vm.stopBroadcast();
}
Expand Down
3 changes: 3 additions & 0 deletions contracts/src/FundAgent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ contract FundAgent is Script {

bytes32 bridgeHubAgentID = vm.envBytes32("BRIDGE_HUB_AGENT_ID");
bytes32 assetHubAgentID = vm.envBytes32("ASSET_HUB_AGENT_ID");
bytes32 penpalAgentID = vm.envBytes32("PENPAL_AGENT_ID");

address bridgeHubAgent = IGateway(gatewayAddress).agentOf(bridgeHubAgentID);
address assetHubAgent = IGateway(gatewayAddress).agentOf(assetHubAgentID);
address penpalAgent = IGateway(gatewayAddress).agentOf(penpalAgentID);

payable(bridgeHubAgent).safeNativeTransfer(initialDeposit);
payable(assetHubAgent).safeNativeTransfer(initialDeposit);
payable(penpalAgent).safeNativeTransfer(initialDeposit);

vm.stopBroadcast();
}
Expand Down
3 changes: 2 additions & 1 deletion contracts/src/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ enum Command {
}

enum AgentExecuteCommand {
TransferToken
TransferToken,
Transact
}

/// @dev Application-level costs for a message
Expand Down
50 changes: 50 additions & 0 deletions contracts/src/utils/Call.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,54 @@ library Call {
}
}
}

/// @notice Use when you _really_ really _really_ don't trust the called
/// contract. This prevents the called contract from causing reversion of
/// the caller in as many ways as we can.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should understand how using zexcessivelySafeCall` can still fail? And potentially have tests for at least one case to make sure we don't poisen pill the inbound queue.

Copy link
Contributor Author

@yrong yrong Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, an unsafe call could be dangerous so we need to be very careful here. Except that I would also like to introduce white-list access control, something like the SafeCallFilter but on Ethereum side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

@vgeddes vgeddes Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts about this:

  1. I think excessivelySafeCall (used by Nomad) is too complicated for our needs. When issuing a call, we are only interested in whether the call succeeded or not. The returndata we can ignore. For this reason, I prefer that we use this safe call solution: https://github.com/ethereum-optimism/optimism/blob/b36eb5515cc2a34a15383b2eee488dbac83d6caf/packages/contracts-bedrock/src/libraries/SafeCall.sol#L12

  2. I am optimistic we've accounted for all the ways a contract call can be unsafe. This includes guards against re-entrancy (nonce check), gas limits, and returnbomb attacks. Therefore I don't see a reason to include a safe call filter. If we think a call can still be unsafe, we need to understand why. For now I would just remove the safe call filter.

Copy link
Collaborator

@musnit musnit Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yrong How does SafeCallFilter work? Will we have to restrict allowing transact to only explicitly specified contracts and/or functions? This won't work - we 100% need permissionless support for anyone to integrate transacting with any contract and any function without permission.

And yeah agree with @vgeddes on just ignoring all returndata

Copy link
Contributor Author

@yrong yrong Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does SafeCallFilter work? Will we have to restrict allowing transact to only explicitly specified contracts and/or functions?

It's not by us or requires governance from the relaychain, but instead from the Parachain sovereign through xcm(i.e. require another call added in our system pallet), they can increase the whitelist step by step entirely under their control.

In this way, we can release the transact support without introducing potential risk.

Copy link
Contributor Author

@yrong yrong Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore returndata 0a8dc1e

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow arbitray transact without any black(white)-list 606e867

/// @dev The main difference between this and a solidity low-level call is
/// that we limit the number of bytes that the callee can cause to be
/// copied to caller memory. This prevents stupid things like malicious
/// contracts returning 10,000,000 bytes causing a local OOG when copying
/// to memory.
/// @param _target The address to call
/// @param _gas The amount of gas to forward to the remote contract
/// @param _value The value in wei to send to the remote contract
/// @param _maxCopy The maximum number of bytes of returndata to copy
/// to memory.
/// @param _calldata The data to send to the remote contract
/// @return success and returndata, as `.call()`. Returndata is capped to
/// `_maxCopy` bytes.
function excessivelySafeCall(address _target, uint256 _gas, uint256 _value, uint16 _maxCopy, bytes memory _calldata)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're no longer using this, can we remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal
returns (bool, bytes memory)
{
// set up for assembly call
uint256 _toCopy;
bool _success;
bytes memory _returnData = new bytes(_maxCopy);
// dispatch message to recipient
// by assembly calling "handle" function
// we call via assembly to avoid memcopying a very large returndata
// returned by a malicious contract
assembly {
_success :=
call(
_gas, // gas
_target, // recipient
_value, // ether value
add(_calldata, 0x20), // inloc
mload(_calldata), // inlen
0, // outloc
0 // outlen
)
// limit our copy to 256 bytes
_toCopy := returndatasize()
if gt(_toCopy, _maxCopy) { _toCopy := _maxCopy }
// Store the length of the copied bytes
mstore(_returnData, _toCopy)
// copy the bytes from returndata[0:_toCopy]
returndatacopy(add(_returnData, 0x20), 0, _toCopy)
}
return (_success, _returnData);
}
}
37 changes: 37 additions & 0 deletions contracts/test/Gateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import {WETH9} from "canonical-weth/WETH9.sol";
import "./mocks/GatewayUpgradeMock.sol";
import {UD60x18, ud60x18, convert} from "prb/math/src/UD60x18.sol";
import {HelloWorld} from "./mocks/HelloWorld.sol";

contract GatewayTest is Test {
ParaID public bridgeHubParaID = ParaID.wrap(1001);
Expand Down Expand Up @@ -91,6 +92,8 @@ contract GatewayTest is Test {
// ETH/DOT exchange rate
UD60x18 public exchangeRate = ud60x18(0.0025e18);

event SaidHello(string indexed message);

function setUp() public {
AgentExecutor executor = new AgentExecutor();
gatewayLogic =
Expand Down Expand Up @@ -825,4 +828,38 @@ contract GatewayTest is Test {
fee = IGateway(address(gateway)).quoteRegisterTokenFee();
assertEq(fee, 10000000000000000);
}

function testAgentExecutionTransact() public {
HelloWorld helloWorld = new HelloWorld();

bytes memory payload = abi.encodeWithSignature("sayHello(string)", "Clara");

AgentExecuteParams memory params = AgentExecuteParams({
agentID: assetHubAgentID,
payload: abi.encode(AgentExecuteCommand.Transact, abi.encode(address(helloWorld), payload, 100000))
});

// Expect the HelloWorld contract to emit `SaidHello`
vm.expectEmit(true, false, false, false);
emit SaidHello("Hello there, Clara");

GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params));
}

function testAgentExecutionTransactFail() public {
HelloWorld helloWorld = new HelloWorld();

bytes memory payload = abi.encodeWithSignature("revertUnauthorized()");

AgentExecuteParams memory params = AgentExecuteParams({
agentID: assetHubAgentID,
payload: abi.encode(AgentExecuteCommand.Transact, abi.encode(address(helloWorld), payload, 100000))
});

vm.expectRevert(
abi.encodeWithSelector(Gateway.AgentExecutionFailed.selector, abi.encodeWithSignature("Unauthorized()"))
);

GatewayMock(address(gateway)).agentExecutePublic(abi.encode(params));
}
}
17 changes: 17 additions & 0 deletions contracts/test/mocks/HelloWorld.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.23;

contract HelloWorld {
event SaidHello(string indexed message);

error Unauthorized();

function sayHello(string memory _text) public {
string memory fullMessage = string(abi.encodePacked("Hello there, ", _text));
emit SaidHello(fullMessage);
}

function revertUnauthorized() public pure {
revert Unauthorized();
}
}
2 changes: 1 addition & 1 deletion smoketest/make-bindings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mkdir -p src/contracts

# Generate Rust bindings for contracts
forge bind --module --overwrite \
--select 'IGateway|WETH9|GatewayUpgradeMock' \
--select 'IGateway|WETH9|GatewayUpgradeMock|HelloWorld' \
--bindings-path src/contracts \
--root ../contracts

Expand Down
83 changes: 83 additions & 0 deletions smoketest/tests/transact_from_penpal_to_ethereum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use ethers::{
abi::{Abi, Token},
prelude::{Address, Middleware, Provider, Ws},
};
use futures::StreamExt;
use hex_literal::hex;
use snowbridge_smoketest::{
contracts::hello_world::{HelloWorld, SaidHelloFilter},
helper::*,
parachains::penpal::api as PenpalApi,
};
use std::{ops::Deref, sync::Arc};
use subxt::{
ext::sp_core::{sr25519::Pair, Pair as PairT},
tx::PairSigner,
};

const HELLO_WORLD_CONTRACT: [u8; 20] = hex!("EE9170ABFbf9421Ad6DD07F6BDec9D89F2B581E0");

#[tokio::test]
async fn transact_from_penpal_to_ethereum() {
let test_clients = initial_clients().await.expect("initialize clients");

let ethereum_client = *(test_clients.ethereum_client.clone());
let penpal_client = *(test_clients.penpal_client.clone());

let hello_world = HelloWorld::new(HELLO_WORLD_CONTRACT, ethereum_client.clone());
let contract_abi: Abi = hello_world.abi().clone();
let function = contract_abi.function("sayHello").unwrap();
let encoded_data =
function.encode_input(&[Token::String("Hello, Clara!".to_string())]).unwrap();
yrong marked this conversation as resolved.
Show resolved Hide resolved

println!("data is {}", hex::encode(encoded_data.clone()));

let extrinsic_call = PenpalApi::transact_helper::calls::TransactionApi.transact_to_ethereum(
HELLO_WORLD_CONTRACT.into(),
encoded_data,
4_000_000_000,
80_000,
);

let owner: Pair = Pair::from_string("//Bob", None).expect("cannot create keypair");
let signer: PairSigner<PenpalConfig, _> = PairSigner::new(owner);

let _ = penpal_client
.tx()
.sign_and_submit_then_watch_default(&extrinsic_call, &signer)
.await
.expect("send through xcm call.");

wait_for_arbitrary_transact_event(&test_clients.ethereum_client, HELLO_WORLD_CONTRACT).await;
}

pub async fn wait_for_arbitrary_transact_event(
ethereum_client: &Box<Arc<Provider<Ws>>>,
contract_address: [u8; 20],
) {
let addr: Address = contract_address.into();
let contract = HelloWorld::new(addr, (*ethereum_client).deref().clone());

let wait_for_blocks = 300;
let mut stream = ethereum_client.subscribe_blocks().await.unwrap().take(wait_for_blocks);

let mut ethereum_event_found = false;
while let Some(block) = stream.next().await {
if let Ok(events) = contract
.event::<SaidHelloFilter>()
.at_block_hash(block.hash.unwrap())
.query()
.await
{
for _ in events {
println!("Event found at ethereum block {:?}", block.number.unwrap());
ethereum_event_found = true;
break
}
}
if ethereum_event_found {
break
}
}
assert!(ethereum_event_found);
}
4 changes: 3 additions & 1 deletion web/packages/test/scripts/set-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ assethub_ws_url="${ASSET_HUB_WS_URL:-ws://127.0.0.1:12144}"
assethub_seed="${ASSET_HUB_SEED:-//Alice}"
export ASSET_HUB_PARAID="${ASSET_HUB_PARAID:-1000}"
export ASSET_HUB_AGENT_ID="${ASSET_HUB_AGENT_ID:-0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79}"

export ASSET_HUB_CHANNEL_ID="0xc173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539"

export PENPAL_AGENT_ID="5097ee1101e90c3aadb882858c59a22108668021ec81bce9f4930155e5c21e59"
export PENPAL_CHANNEL_ID="0xa69fbbae90bb6096d59b1930bbcfc8a3ef23959d226b1861deb7ad8fb06c6fa3"

export PRIMARY_GOVERNANCE_CHANNEL_ID="0x0000000000000000000000000000000000000000000000000000000000000001"
export SECONDARY_GOVERNANCE_CHANNEL_ID="0x0000000000000000000000000000000000000000000000000000000000000002"

Expand Down
Loading