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

Contracts V2 #1300

Open
wants to merge 23 commits into
base: v2
Choose a base branch
from
Open

Contracts V2 #1300

wants to merge 23 commits into from

Conversation

vgeddes
Copy link
Collaborator

@vgeddes vgeddes commented Oct 7, 2024

Tasks:

  • V2 types
  • V2 inbound messaging
  • V2 outbound messaging
  • Refactor to support both V1 & V2
  • Re-implement native token registration for V2 & Kusama compatibility
  • Auto-wrap ether for Ethereum->Polkadot
  • Auto-unwrap ether for Polkadot->Ethereum
  • Generate encoded XCM fragments for token registration
  • Split IGateway into IGatewayV1 and IGatewayV2
  • Remove duplicated error definitions scattered throughout code
  • Unit tests
  • Code documentation

contracts/src/types/V2.sol Outdated Show resolved Hide resolved
contracts/src/types/V2.sol Outdated Show resolved Hide resolved
@yrong yrong mentioned this pull request Oct 15, 2024
8 tasks
@yrong yrong mentioned this pull request Oct 18, 2024
8 tasks
@vgeddes vgeddes changed the title V2 Contracts V2 Oct 18, 2024
@vgeddes vgeddes changed the base branch from main to v2 October 19, 2024 09:56
@vgeddes vgeddes marked this pull request as ready for review October 19, 2024 09:56
Copy link

codecov bot commented Oct 19, 2024

Codecov Report

Attention: Patch coverage is 51.76152% with 178 lines in your changes missing coverage. Please review.

Please upload report for BASE (v2@6d11768). Learn more about missing BASE report.

Files with missing lines Patch % Lines
contracts/src/Gateway.sol 27.04% 88 Missing and 1 partial ⚠️
contracts/src/v2/Calls.sol 0.00% 50 Missing ⚠️
contracts/src/v2/Handlers.sol 0.00% 21 Missing ⚠️
contracts/src/Functions.sol 80.95% 8 Missing ⚠️
contracts/src/v1/Calls.sol 90.90% 5 Missing ⚠️
contracts/src/AgentExecutor.sol 0.00% 3 Missing ⚠️
contracts/src/upgrades/polkadot/Gateway202411.sol 33.33% 2 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff          @@
##             v2    #1300   +/-   ##
=====================================
  Coverage      ?   60.22%           
=====================================
  Files         ?       23           
  Lines         ?      714           
  Branches      ?      120           
=====================================
  Hits          ?      430           
  Misses        ?      275           
  Partials      ?        9           
Flag Coverage Δ
solidity 60.22% <51.76%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

_ensureOutboundMessagingEnabled();

// Wrap provided ether and transfer to AssetHub agent
address assetHubAgent = Functions.ensureAgent(Constants.ASSET_HUB_AGENT_ID);
Copy link
Contributor

@yrong yrong Oct 20, 2024

Choose a reason for hiding this comment

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

Since we know agent of asset-hub do exists in production. Is this check nessessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah the check is not necessary, I'll add a TODO.

contracts/src/v2/Calls.sol Outdated Show resolved Hide resolved
Comment on lines 97 to 98
bytes memory payload =
SubstrateTypes.encodePayloadV2(ticket.origin, ticket.assets, ticket.xcm);
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, A lot more simple now!

With ticket.xcm does that mean for V2 there is no need to construct xcm on chain any more?

UI will build the xcm and it's the responsibility of relayer to validate the xcm and check the relay is profitable or not.

On BH we just decode the xcm with

VersionedXcm::<()>::decode_with_depth_limit(
    MAX_XCM_DECODE_DEPTH,
    &mut data,
)

and forward it to AH transparently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, exactly.

Though for registration of native tokens, we need to control the XCM and so we need to build it on chain. See more details in 149c3e4.

Copy link
Contributor

Choose a reason for hiding this comment

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

Though for registration of native tokens, control the XCM and so we need to build it on chain

Still not sure fully understand why ticket.assets is required here, why not just build the xcm off chain?

@vgeddes I've removed the MessageConverter completely in yrong/polkadot-sdk#3 which I'd assume is not in use any more, correct me if I'm wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Still not sure fully understand why ticket.assets is required here, why not just build the xcm off chain?

When the message gets to BH, we convert the payload.assets to instructions like ReserveAssetDeposited,etc, and then append the instructions in payload.xcm.

Or in other words, BH builds final XCM using:

finalXCM = concat(
assets_to_xcm(payload.asssets),
UniversalOrigin(Ethereum)
DescendOrigin(payload.origin)
payload.xcm
)

We can't trust the user to build the entire xcm offchain as they could mint any assets they want. Rather, they can only supply a fragment of the xcm.

Copy link
Contributor

@yrong yrong Oct 20, 2024

Choose a reason for hiding this comment

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

We can't trust the user to build the entire xcm offchain as they could mint any assets they want.

Yeah, that make sense.

Instead of convert the assets to xcm, can we just do some basic check on BH to verify the xcm is constructed correctly based upon the assets? e.g.

ensure!(xcm contains DescendOrigin(payload.origin))
ensure!(xcm contains ReserveAssetDeposited(asset)|WithdrawAsset(asset)) // Depends on the transfer type is ENA or PNA
...

So the point here is on chain we do validate rather than construct.

Copy link
Contributor

@yrong yrong Oct 21, 2024

Choose a reason for hiding this comment

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

Btw: for transfer I'm not sure DescendOrigin(userOrigin) make sense. For PNA we need to burn the asset locked in Ethereum sovereign on AH. IIUC that's why we change origin to UniversalOrigin(GlobalConsensus(network)) before WithdrawAsset here.

Copy link
Contributor

Choose a reason for hiding this comment

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

So the point here is on chain we do validate rather than construct.

The issue with this approach is that the validation needs to happen on the Ethereum side in order to do this right. The actual locking/unlocking/minting/burning of tokens occurs on the Ethereum side in solidity, which must match the Assets section of the XCM. If the validation happens on BH or in a relayer, and we reject the message, it is too late because we have already changed the state on the Ethereum side. This is why it is better to build the assets section of the XCM on BH to match what was done on the Ethereum side.

Related comment here: yrong/polkadot-sdk#3 (comment)

Copy link
Contributor

@yrong yrong Oct 25, 2024

Choose a reason for hiding this comment

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

it is too late because we have already changed the state on the Ethereum side

Just curious what will happen in this case. Is this something acceptable assuming we will implement error handing mechanism any way? The message with errored xcm will get trapped on BH and we allow end user to edit/repair it?

Something like we allow user to add fee for message pending from Substrate to Ethereum direction, assuming there is no relayer who will pick it up if it's not profitable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious what will happen in this case. Is this something acceptable assuming we will implement error handing mechanism any way?

I think if the message is queued and if the message is profitable, than once submitted to BH we should not fail for any reason (such as validating the xcm for example) and we should try to always push the assets to AH where they can get trapped if the user passes invalid xcm. And we should trust that we constrain the origin to stop users from doing things that they cannot within the xcm, instead of validating.

contracts/src/v2/Calls.sol Outdated Show resolved Hide resolved
/// @param leafProof A message proof used to verify that the message is in the merkle tree committed by the OutboundQueue pallet
/// @param headerProof A proof that the commitment is included in parachain header that was finalized by BEEFY.
/// @param rewardAddress Account on BH to credit delivery rewards
function v2_submit(
Copy link
Contributor

@yrong yrong Oct 22, 2024

Choose a reason for hiding this comment

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

should it also be in IGateway.sol? Meanwhile we may need another function isRelayed(nonce) to check if the nonce has been relayed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch!


struct Command {
uint8 kind;
uint256 gas;
Copy link
Contributor

Choose a reason for hiding this comment

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

Gas is uint64.

contracts/src/Constants.sol Show resolved Hide resolved

function testGetSet() public {
bitmap.set(384);
assertEq(bitmap.get(384), true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe test the bits around the set bit to make sure they remain unset.

Suggested change
assertEq(bitmap.get(384), true);
assertEq(bitmap.get(383), false);
assertEq(bitmap.get(384), true);
assertEq(bitmap.get(385), false);

Comment on lines 97 to 98
bytes memory payload =
SubstrateTypes.encodePayloadV2(ticket.origin, ticket.assets, ticket.xcm);
Copy link
Contributor

Choose a reason for hiding this comment

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

So the point here is on chain we do validate rather than construct.

The issue with this approach is that the validation needs to happen on the Ethereum side in order to do this right. The actual locking/unlocking/minting/burning of tokens occurs on the Ethereum side in solidity, which must match the Assets section of the XCM. If the validation happens on BH or in a relayer, and we reject the message, it is too late because we have already changed the state on the Ethereum side. This is why it is better to build the assets section of the XCM on BH to match what was done on the Ethereum side.

Related comment here: yrong/polkadot-sdk#3 (comment)


// Register Ethereum-native token on AHK, using `xcmFeeAHP` and `xcmFeeAHK`
// of `msg.value` to pay for execution on AHP and AHK respectively.
function registerTokenOnKusama(address token, uint128 xcmFeeAHP, uint128 xcmFeeAHK)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should introduce a Network Enum here as a destination parameter, instead of having 2 registers methods. The network can simply contain the Polkadot networks.
https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/xcm/src/v4/junction.rs/#L125

error TokenNotRegistered();
error InvalidAsset();

address public constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hardcoded WETH_ADDRESS?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, why would it need to be mutable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, I see, for Ethereum testnets, WETH will have a different address. got it. 👍

revert ChannelDoesNotExist();
}
// See docs for `IGateway.sendMessage`
function sendMessage(bytes calldata xcm, bytes[] calldata assets) external payable {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should add a MultiAddress parameter that can be the asset claimer. If provided then we can use this account with the SetClaimer XCM instruction. If it is zero/empty address then we SetClaimer to the origin(msg.sender).


emit IGateway.OutboundMessageAccepted(channelID, channel.outboundNonce, messageID, ticket.payload);
emit IGateway.InboundMessageDispatched(message.nonce, success, rewardAddress);
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be better to store the delivery status on chain? e.g:

mapping(uint64 nonce => DeliveryStatus) public nonces;
struct DeliveryStatus {
    uint64 blockNumber;
    bool result;
}

With the blockNumber it's easy for relayer to filter this event efficiently when submit the delivery proof.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a good point Ron. Though there are some simplifying assumptions which I think allow us to save 20000 gas by not storing that status on-chain.

The plan I had in mind was that the relayer who calls v2_submit is also responsible for sending delivery proof back to BH. Since this relayer witnessed the result for v2_submit it can generate a proof for InboundMessageDispatched, and there isn't a need for additional metadata on-chain.

For other cases when the delivery proof could not be sent back immediately, we could create a command in snowbridge-relay that queries for InboundMessageDispatched(rewardAddress=X) against some public ethereum node or event indexer, and sends the delivery proofs for the matching events.

I will make a rewardAddress an indexed event parameter, which allows this query to be much faster to execute.

Copy link
Contributor

Choose a reason for hiding this comment

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

import {OperatingMode} from "./../types/Common.sol";

// Inbound message from a Polkadot parachain (via BridgeHub)
struct InboundMessage {
Copy link
Contributor

@yrong yrong Oct 28, 2024

Choose a reason for hiding this comment

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

would suggest to rename as InboundMessageV2 or conflict error when generate go bindings.

 > mage build
2024/10/28 15:53:26 INFO: Skipped ssz generated object: relays/beacon/state/beacon_encoding.go
2024/10/28 15:53:26 INFO: Skipped ssz generated object: relays/beacon/state/beacon_deneb_encoding.go
# github.com/snowfork/snowbridge/relayer/contracts
contracts/gateway.go:47:6: InboundMessage redeclared in this block
	contracts/gateway.go:40:6: other declaration of InboundMessage
Error: running "go build -o build/snowbridge-relay main.go" failed with exit code 1

Copy link
Contributor

Choose a reason for hiding this comment

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

af374a6 FYI

) external payable;

// Check if an inbound message was previously accepted and dispatched
function v2_isDispatched(uint64 nonce) external returns (bool);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should add view keyword here.

2e8d519


// Register Ethereum-native token on AHK, using `xcmFeeAHP` and `xcmFeeAHK`
// of `msg.value` to pay for execution on AHP and AHK respectively.
function v2_registerTokenOnKusama(
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a thought on registration while thinking about our Security model. Kusama token registration can be built as a separate contract on top of the gateway.

The implementation of a contract called KusamaRegistration, that can build the registration Transact call onchain, and then submit it to the gateway.

On BH we would decend origin to that Location so the origin would be { parents:2 interior: X2(Ethreum{chain_id:1}, KusamaRegistration)}.

This origin would be forwarded to AHK using AliasOrigin, and can be used secure the register extrinsic on the foreignAssets pallet on AHK. This is currently how we secure token registration now on AHP, using the gateway contracts origin (Snowbridge Sovereign).

Upgrade would be just deploying a new version of the KusamaRegistration contract and then updating the origin which is allowed to call register on AHK. And it would be separate from the Gateway.

Copy link
Contributor

@claravanstaden claravanstaden left a comment

Choose a reason for hiding this comment

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

Nice!

I was wondering if it would be feasible to add Transact support in this release, given that all the building blocks are there?

@@ -59,7 +56,9 @@ contract UpgradeShell is Script {
assetHubReserveTransferFee: mDot(100), // 0.1 DOT
exchangeRate: ud60x18(0.0024e18),
multiplier: ud60x18(1.33e18),
rescueOperator: 0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967
rescueOperator: 0x4B8a782D4F03ffcB7CE1e95C5cfe5BFCb2C8e967,
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably don't need this anymore, since we have already dropped the operator on mainnet?

Comment on lines +8 to +12
ParaID constant ASSET_HUB_PARA_ID = ParaID.wrap(1000);
bytes32 constant ASSET_HUB_AGENT_ID =
0x81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79;

ParaID constant BRIDGE_HUB_PARA_ID = ParaID.wrap(1002);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! Seems like these para IDs are the same on all the environments now anyway.

try Gateway(this).createChannel{gas: maxDispatchGas}(message.params) {}
catch {
} else if (message.command == CommandV1.CreateChannel) {
success = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

So we essentially disable creating new channels?

UpdateChannelParams memory params = abi.decode(data, (UpdateChannelParams));
// @dev Get token address by tokenID
function tokenAddressOf(bytes32 tokenID) external view returns (address) {
return CallsV1.tokenAddressOf(tokenID);
Copy link
Contributor

Choose a reason for hiding this comment

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

How come this is constrained to v1? Would it not stay the same for v2?

emit ChannelUpdated(params.channelID);
/// @dev Create an agent for a consensus system on Polkadot
function v1_handleCreateAgent(bytes calldata data) external onlySelf {
HandlersV1.createAgent(data);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is there an extra layer of functions for these commands? Wouldn't it be fine to use HandlersV1.createAgent(data); directly in https://github.com/Snowfork/snowbridge/pull/1300/files#diff-74470ba8f35b42dbc8805739e92708c0696343e0b1819feb3e7a5ebb77cc623cR168?

Copy link
Contributor

Choose a reason for hiding this comment

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

try / catch can only catch errors from external function calls.

function _submitOutbound(Ticket memory ticket) internal {
ChannelID channelID = ticket.dest.into();
Channel storage channel = _ensureChannel(channelID);
uint256 public constant DISPATCH_OVERHEAD_GAS_V2 = 32_000;
Copy link
Contributor

Choose a reason for hiding this comment

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

How do you calculate this? Comment about it might be useful.

@@ -16,16 +16,7 @@ contract RococoGatewayV2 is Gateway {
bytes32 bridgeHubAgentID,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: File should probably be for Westend now that Rococo is deprecated.

* Internal functions
*/

// Convert foreign currency to native currency (ROC/KSM/DOT -> ETH)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Convert foreign currency to native currency (ROC/KSM/DOT -> ETH)
// Convert foreign currency to native currency (WND/PAS/KSM/DOT -> ETH)

contracts/src/v2/Calls.sol Show resolved Hide resolved
contracts/src/v2/Types.sol Show resolved Hide resolved
InboundMessageV2 calldata message,
bytes32[] calldata leafProof,
Verification.Proof calldata headerProof,
bytes32 rewardAddress
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't we just just derive the account from the origin on BH?

Copy link
Contributor

@yrong yrong Nov 4, 2024

Choose a reason for hiding this comment

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

It's different. The origin in InboundMessage is derived from account on source chain, while the rewardAddress is account of the relayer for accepting reward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants