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

Support Native ETH in v1 #1354

Open
wants to merge 22 commits into
base: main
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
10 changes: 3 additions & 7 deletions contracts/scripts/DeployLocal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,11 @@ contract DeployLocal is Script {
// Deploy WETH for testing
new WETH9();

// Fund the sovereign account for the BridgeHub parachain. Used to reward relayers
// Fund the gateway proxy contract. Used to reward relayers
// of messages originating from BridgeHub
uint256 initialDeposit = vm.envUint("BRIDGE_HUB_INITIAL_DEPOSIT");
uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT");

address bridgeHubAgent = IGateway(address(gateway)).agentOf(bridgeHubAgentID);
address assetHubAgent = IGateway(address(gateway)).agentOf(assetHubAgentID);

payable(bridgeHubAgent).safeNativeTransfer(initialDeposit);
payable(assetHubAgent).safeNativeTransfer(initialDeposit);
IGateway(address(gateway)).depositEther{value: initialDeposit}();

// Deploy MockGatewayV2 for testing
new MockGatewayV2();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {ParaID} from "../src/Types.sol";
import {SafeNativeTransfer} from "../src/utils/SafeTransfer.sol";
import {stdJson} from "forge-std/StdJson.sol";

contract FundAgent is Script {
contract FundGateway is Script {
using SafeNativeTransfer for address payable;
using stdJson for string;

Expand All @@ -26,17 +26,10 @@ contract FundAgent is Script {
address deployer = vm.rememberKey(privateKey);
vm.startBroadcast(deployer);

uint256 initialDeposit = vm.envUint("BRIDGE_HUB_INITIAL_DEPOSIT");
uint256 initialDeposit = vm.envUint("GATEWAY_PROXY_INITIAL_DEPOSIT");
address gatewayAddress = vm.envAddress("GATEWAY_PROXY_CONTRACT");

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

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

payable(bridgeHubAgent).safeNativeTransfer(initialDeposit);
payable(assetHubAgent).safeNativeTransfer(initialDeposit);
IGateway(address(gatewayAddress)).depositEther{value: initialDeposit}();

vm.stopBroadcast();
}
Expand Down
3 changes: 1 addition & 2 deletions contracts/src/Agent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ contract Agent {
}

/// @dev Agents can receive ether permissionlessly.
/// This is important, as agents for top-level parachains also act as sovereign accounts from which message relayers
/// are rewarded.
/// This is important, as agents are used to lock ether.
receive() external payable {}

/// @dev Allow the gateway to invoke some code within the context of this agent
Expand Down
7 changes: 6 additions & 1 deletion contracts/src/AgentExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {AgentExecuteCommand, ParaID} from "./Types.sol";
import {SubstrateTypes} from "./SubstrateTypes.sol";

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

/// @title Code which will run within an `Agent` using `delegatecall`.
Expand All @@ -16,11 +17,15 @@ contract AgentExecutor {

/// @dev Transfer ether to `recipient`. Unlike `_transferToken` This logic is not nested within `execute`,
/// as the gateway needs to control an agent's ether balance directly.
///
function transferNative(address payable recipient, uint256 amount) external {
recipient.safeNativeTransfer(amount);
}

/// @dev Transfer ether to Gateway. Used once off for migration purposes. Can be removed after version 1.
function transferNativeToGateway(address payable gateway, uint256 amount) external {
IGateway(gateway).depositEther{value: amount}();
}

/// @dev Transfer ERC20 to `recipient`. Only callable via `execute`.
function transferToken(address token, address recipient, uint128 amount) external {
_transferToken(token, recipient, amount);
Expand Down
40 changes: 32 additions & 8 deletions contracts/src/Assets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {CoreStorage} from "./storage/CoreStorage.sol";
import {SubstrateTypes} from "./SubstrateTypes.sol";
import {ParaID, MultiAddress, Ticket, Costs} from "./Types.sol";
import {Address} from "./utils/Address.sol";
import {SafeNativeTransfer} from "./utils/SafeTransfer.sol";
import {AgentExecutor} from "./AgentExecutor.sol";
import {Agent} from "./Agent.sol";
import {Call} from "./utils/Call.sol";
Expand All @@ -21,6 +22,7 @@ import {Token} from "./Token.sol";
/// @title Library for implementing Ethereum->Polkadot ERC20 transfers.
library Assets {
using Address for address;
using SafeNativeTransfer for address payable;
using SafeTokenTransferFrom for IERC20;

/* Errors */
Expand Down Expand Up @@ -60,7 +62,7 @@ library Assets {
) external view returns (Costs memory costs) {
AssetsStorage.Layout storage $ = AssetsStorage.layout();
TokenInfo storage info = $.tokenRegistry[token];
if (!info.isRegistered) {
if (!info.isRegistered && token != address(0)) {
revert TokenNotRegistered();
}

Expand Down Expand Up @@ -110,15 +112,17 @@ library Assets {

TokenInfo storage info = $.tokenRegistry[token];

if (!info.isRegistered) {
revert TokenNotRegistered();
}

if (info.foreignID == bytes32(0)) {
if (!info.isRegistered && token != address(0)) {
revert TokenNotRegistered();
}
return _sendNativeToken(
token, sender, destinationChain, destinationAddress, destinationChainFee, maxDestinationChainFee, amount
);
} else {
if (!info.isRegistered) {
revert TokenNotRegistered();
}
return _sendForeignToken(
info.foreignID,
token,
Expand All @@ -144,7 +148,18 @@ library Assets {
AssetsStorage.Layout storage $ = AssetsStorage.layout();

// Lock the funds into AssetHub's agent contract
_transferToAgent($.assetHubAgent, token, sender, amount);
if (token != address(0)) {
// ERC20
_transferToAgent($.assetHubAgent, token, sender, amount);
ticket.value = 0;
} else {
// Native ETH
if (msg.value < amount) {
revert InvalidAmount();
}
payable($.assetHubAgent).safeNativeTransfer(amount);
ticket.value = amount;
}

ticket.dest = $.assetHubParaID;
ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee);
Expand Down Expand Up @@ -211,6 +226,7 @@ library Assets {

ticket.dest = $.assetHubParaID;
ticket.costs = _sendTokenCosts(destinationChain, destinationChainFee, maxDestinationChainFee);
ticket.value = 0;

// Construct a message payload
if (destinationChain == $.assetHubParaID && destinationAddress.isAddress32()) {
Expand Down Expand Up @@ -254,14 +270,15 @@ library Assets {
// It means that registration can be retried.
// But register a PNA here is not allowed
TokenInfo storage info = $.tokenRegistry[token];
if(info.foreignID != bytes32(0)) {
if (info.foreignID != bytes32(0)) {
revert TokenAlreadyRegistered();
}
info.isRegistered = true;

ticket.dest = $.assetHubParaID;
ticket.costs = _registerTokenCosts();
ticket.payload = SubstrateTypes.RegisterToken(token, $.assetHubCreateAssetFee);
ticket.value = 0;

emit IGateway.TokenRegistrationSent(token);
}
Expand Down Expand Up @@ -293,7 +310,14 @@ library Assets {
function transferNativeToken(address executor, address agent, address token, address recipient, uint128 amount)
external
{
bytes memory call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount));
bytes memory call;
if (token != address(0)) {
// ERC20
call = abi.encodeCall(AgentExecutor.transferToken, (token, recipient, amount));
} else {
// Native ETH
call = abi.encodeCall(AgentExecutor.transferNative, (payable(recipient), amount));
}
(bool success,) = Agent(payable(agent)).invoke(executor, call);
if (!success) {
revert TokenTransferFailed();
Expand Down
32 changes: 21 additions & 11 deletions contracts/src/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ contract Gateway is IGateway, IInitializable, IUpgradable {
error InvalidProof();
error InvalidNonce();
error NotEnoughGas();
error FeePaymentToLow();
error FeePaymentTooLow();
error Unauthorized();
error Disabled();
error AgentAlreadyCreated();
Expand Down Expand Up @@ -240,11 +240,11 @@ contract Gateway is IGateway, IInitializable, IUpgradable {

// Add the reward to the refund amount. If the sum is more than the funds available
// in the channel agent, then reduce the total amount
uint256 amount = Math.min(refund + message.reward, address(channel.agent).balance);
uint256 amount = Math.min(refund + message.reward, address(this).balance);

// Do the payment if there funds available in the agent
// Do the payment if there funds available in the gateway
if (amount > _dustThreshold()) {
_transferNativeFromAgent(channel.agent, payable(msg.sender), amount);
payable(msg.sender).safeNativeTransfer(amount);
}

emit IGateway.InboundMessageDispatched(message.channelID, message.nonce, message.id, success);
Expand Down Expand Up @@ -280,6 +280,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable {
return ERC1967.load();
}

function version() public view returns (uint64) {
return CoreStorage.layout().version;
}

/**
* Fee management
*/
function depositEther() external payable {
emit EtherDeposited(msg.sender, msg.value);
}

/**
* Handlers
*/
Expand Down Expand Up @@ -530,18 +541,17 @@ contract Gateway is IGateway, IInitializable, IUpgradable {
uint256 fee = _calculateFee(ticket.costs);

// Ensure the user has enough funds for this message to be accepted
if (msg.value < fee) {
revert FeePaymentToLow();
uint256 totalEther = fee + ticket.value;
if (msg.value < totalEther) {
revert FeePaymentTooLow();
}

channel.outboundNonce = channel.outboundNonce + 1;

// Deposit total fee into agent's contract
payable(channel.agent).safeNativeTransfer(fee);

// The fee is already collected into the gateway contract
// Reimburse excess fee payment
if (msg.value > fee) {
payable(msg.sender).safeNativeTransfer(msg.value - fee);
if (msg.value > totalEther && (msg.value - totalEther) > _dustThreshold()) {
payable(msg.sender).safeNativeTransfer(msg.value - totalEther);
}

// Generate a unique ID for this message
Expand Down
1 change: 1 addition & 0 deletions contracts/src/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ struct Ticket {
ParaID dest;
Costs costs;
bytes payload;
uint128 value;
}

struct TokenInfo {
Expand Down
10 changes: 10 additions & 0 deletions contracts/src/interfaces/IGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ interface IGateway {
// Emitted when foreign token from polkadot registed
event ForeignTokenRegistered(bytes32 indexed tokenID, address token);

// Emitted when ether is deposited
event EtherDeposited(address who, uint256 amount);

/**
* Getters
*/
Expand All @@ -53,6 +56,13 @@ interface IGateway {

function implementation() external view returns (address);

function version() external view returns (uint64);

/**
* Fee management
*/
function depositEther() external payable;

/**
* Messaging
*/
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/storage/CoreStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ library CoreStorage {
mapping(bytes32 agentID => address) agents;
// Agent addresses
mapping(address agent => bytes32 agentID) agentAddresses;
// Version of the Gateway Implementation
uint64 version;
}

bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.core");
Expand Down
15 changes: 14 additions & 1 deletion contracts/src/upgrades/Gateway202410.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,23 @@ contract Gateway202410 is Gateway {
{}

// Override parent initializer to prevent re-initialization of storage.
function initialize(bytes memory) external view override {
function initialize(bytes memory) external override {
// Ensure that arbitrary users cannot initialize storage in this logic contract.
if (ERC1967.load() == address(0)) {
revert Unauthorized();
}

// We expect version 0, deploying version 1.
CoreStorage.Layout storage $ = CoreStorage.layout();
if ($.version != 0) {
revert Unauthorized();
}
$.version = 1;

// migrate asset hub agent
address agent = _ensureAgent(hex"81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79");
bytes memory call =
abi.encodeCall(AgentExecutor.transferNativeToGateway, (payable(address(this)), agent.balance));
_invokeOnAgent(agent, call);
}
}
1 change: 0 additions & 1 deletion contracts/test/Bitfield.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ contract BitfieldTest is Test {

string memory json = vm.readFile(string.concat(vm.projectRoot(), "/test/data/beefy-validator-set.json"));
uint32 setSize = uint32(json.readUint(".validatorSetSize"));
bytes32 root = json.readBytes32(".validatorRoot");
uint256[] memory bitSetArray = json.readUintArray(".participants");

uint256[] memory initialBitField = bw.createBitfield(bitSetArray, setSize);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contract ForkUpgradeTest is Test {
address private constant GatewayProxy = 0x27ca963C279c93801941e1eB8799c23f407d68e7;
address private constant BeefyClient = 0x6eD05bAa904df3DE117EcFa638d4CB84e1B8A00C;
bytes32 private constant BridgeHubAgent = 0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314;
bytes32 private constant AssetHubAgent = 0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79;

function setUp() public {
vm.createSelectFork("https://rpc.tenderly.co/fork/b77e07b8-ad6d-4e83-b5be-30a2001964aa", 20645700);
Expand All @@ -33,10 +34,20 @@ contract ForkUpgradeTest is Test {
UpgradeParams memory params =
UpgradeParams({impl: address(newLogic), implCodeHash: address(newLogic).codehash, initParams: bytes("")});

vm.expectEmit(true, false, false, false);
Gateway gateway = Gateway(GatewayProxy);

// Check pre-migration of ETH from Asset Hub agent
assertGt(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0);
// Check pre-migration of ETH to Gateway
assertEq(address(GatewayProxy).balance, 0);

vm.expectEmit();
emit IGateway.EtherDeposited(gateway.agentOf(AssetHubAgent), 587928061927368450);

vm.expectEmit();
emit IUpgradable.Upgraded(address(newLogic));

Gateway(GatewayProxy).upgrade(abi.encode(params));
gateway.upgrade(abi.encode(params));
}

function checkLegacyToken() public {
Expand All @@ -60,6 +71,12 @@ contract ForkUpgradeTest is Test {
}

function testSanityCheck() public {
// Check that the version is correctly set.
assertEq(IGateway(GatewayProxy).version(), 1);
// Check migration of ETH from Asset Hub agent
assertEq(IGateway(GatewayProxy).agentOf(AssetHubAgent).balance, 0);
// Check migration of ETH to Gateway
assertGt(address(GatewayProxy).balance, 0);
// Check AH channel nonces as expected
(uint64 inbound, uint64 outbound) = IGateway(GatewayProxy).channelNoncesOf(
ChannelID.wrap(0xc173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539)
Expand Down
Loading
Loading