Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

ERC20: enable a subnet's supply to be determined by an ERC20 token in the parent. #313

Merged
merged 30 commits into from
Dec 19, 2023

Conversation

raulk
Copy link
Contributor

@raulk raulk commented Dec 10, 2023

Context

Various IPC users seek to create dedicated crypoeconomies backed by their own token. They'd like to launch their token on another network and inject it into their subnet as native circulating supply, so it can be used (a) to pay for gas, and (b) as the native coin for value transfers.

Contribution

This PR introduces the notion of the "supply source" at the parent subnet. This configuration object determines the kind of supply backing a child subnet. It can be "native" or "ERC20". In the latter case, the token address must be provided. We verify that the address indeed corresponds to an ERC20 token.

We also add a funcWithToken() external non-payable function to the GatewayManagerFacet. This is analogous to fund(), but for token supply (where the amount must be passed as a parameter).

We block message propagation (when the feature flag is enabled) whenever ERC20 networks are involved, as a sender, receiver, or intermediary network.

Non-goals

This PR does NOT introduce the ability to use an ERC20 token as validator stake. We don't expect that feature to be useful at all, since a major benefit of IPC is that subnets draw economic security from the value of the collateral locked, which is denominated in the root coin.

Design doc

https://www.notion.so/pl-strflt/ERC20-Native-coin-conversion-eab9267445b6411d96ec17ef02a6510e?pvs=4

TODO

  • Prevent horizontal subnet xnet when subnet is using ERC20 supply.
  • Releasing/withdrawing funds.
  • Unit tests. (preferring integration tests with a real ERC20 token involved)
  • Integration tests (with TODOs for pending test cases)

@github-actions github-actions bot changed the base branch from main to dev December 10, 2023 23:14
Copy link

Your PR was set to target main. PRs should be target dev.
The base branch of this PR has been automatically changed to dev.
If you really intend to target main, edit the PR.

@raulk raulk force-pushed the raulk/feat/erc20 branch 4 times, most recently from fe94f85 to fe6ae46 Compare December 10, 2023 23:54
revert InvalidERC20Address();
}
// We require that the ERC20 token exists beforehand.
// The call to balanceOf will revert if the supplied address does not exist, or if it's not an ERC20 contract.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice trick!

src/gateway/GatewayManagerFacet.sol Outdated Show resolved Hide resolved
@@ -142,6 +147,12 @@ contract GatewayManagerFacet is GatewayActorModifiers, ReentrancyGuard {
// prevent spamming if there's no value to fund.
revert InvalidCrossMsgValue();
}
// Validate that the supply strategy is native.
Copy link
Contributor

Choose a reason for hiding this comment

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

fund description now is outdated

@@ -142,6 +147,12 @@ contract GatewayManagerFacet is GatewayActorModifiers, ReentrancyGuard {
// prevent spamming if there's no value to fund.
revert InvalidCrossMsgValue();
}
// Validate that the supply strategy is native.
SupplyStrategy memory supplyStrategy = SubnetActorGetterFacet(subnetId.getActor()).supplyStrategy();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe define a getter like IsSubnetWithNativeSupply ?

@@ -154,6 +165,31 @@ contract GatewayManagerFacet is GatewayActorModifiers, ReentrancyGuard {
LibGateway.commitTopDownMsg(crossMsg);
}

function fundWithToken(SubnetID calldata subnetId, FvmAddress calldata to, uint256 amount) external nonReentrant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we validate the address to here? Zero address?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so? 0x00..00 is generally used as a burn address and I don't see why we should specifically prevent deposits straight into the 0x00..00 address. I'm not aware of ERC20 code that prevent such transfers in Ethereum. cc @snissn

Copy link
Contributor

Choose a reason for hiding this comment

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

0x0 has a "special case-ness" in ethereum and i'm not 100% familiar with all of the mechanics to it. Here's some discussion -- https://forum.openzeppelin.com/t/removing-address-0x0-checks-from-openzeppelin-contracts/2222/2

interestingly it seems to regard 0x0 as the "burn address" and wants to separate semantics around transfer and burn. we may want to consider 0x0 as our burn address and welcoming this standard

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comments in that discussion sum it up pretty well. Various standards have coalesced towards treating the 0x0 address as the creator and burn sink of things. I think blocking 0x0 at this level may hinder legitimate user flows down the line. That said, enforcing the restriction now and then loosening it up is more backwards compatible than the other way around...

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe worth considering consensus-shipyard/fendermint#409 has introduced 0x0 for the system address, so at least it works as a source. Hopefully the fact that it doesn't have an actual Ethereum account doesn't interfere with using it as a burn address.

@raulk raulk force-pushed the raulk/feat/erc20 branch 2 times, most recently from 3e930a3 to ebefde3 Compare December 14, 2023 15:30
@snissn
Copy link
Contributor

snissn commented Dec 16, 2023

For tests you mentioned them being incomplete, here are the tests i can think of to add:

  • assert fail funding a native subnet with erc20
  • assert fail funding an erc20 subnet with native
  • test using a real erc20 as opposed to a mock
  • test withdrawing from the subnet (is that possible?)
  • built hardhat entry points and makefile commands that execute code and use that as a test

bool reject;
if (isLCA) {
// We're connecting both networks, so we check them directly.
reject = from.getActor().hasSupplyOfKind(SupplyKind.ERC20);
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 this is wrong, if we assume that multi-level is enabled you may be committing a message locally in a subnet, but you don't have access to the actors of the from or the to because you are an intermediate subnet and they have nothing to you with you. You only need to forward the message, but you know nothing about the source or origin subnets. It should be fine for now, but we should add a not or something to flag it and fix it in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@adlrocha yep, this check only seems to catch sibling to sibling xnet comms (where the first hop is to your LCA). I'll adjust this. Thanks for catching!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the bottom-up path or the pivot, I'd love to catch if the arrivingFrom network was an ERC20 network and reject the message then. However, unless we save the forwarder network in the postbox, we can't do this under propagate. I don't want to do it in _applyMsg because that would make a whole batch fail. I'll open an issue for this, as I don't think it's worth refactoring the postbox right now to store both the arrivingFrom and the CrossMsg.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// Else, commit a bottom up message.
LibGateway.commitBottomUpMsg(crossMessage);
// gas-opt: original check: value > 0
return (shouldBurn = crossMessage.message.value != 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

This assumes that the message value includes the fee to be paid (which is currently the case, but worth noting in case we change the way it works in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think I changed anything here. Do you want to open an issue to track this?

function lock(SupplySource memory supplySource, address from, uint256 value) internal {
if (supplySource.kind == SupplyKind.ERC20) {
IERC20 token = IERC20(supplySource.tokenAddress);
token.safeTransferFrom({from: from, to: address(this), value: value});
Copy link
Contributor

Choose a reason for hiding this comment

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

This assumes a previous approve from the user to the gateway, right? Should we document it explicitly somewhere so we don't forget to add it to the docs and is clear for auditors (or anyone else reading the code?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is also how bridges work more generally, but agree we should document.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added soldocs in the IGateway interface.

Copy link
Contributor

@adlrocha adlrocha left a comment

Choose a reason for hiding this comment

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

Left a few comments. Apart from something that I feel may be wrong when handling multi-level propagations (and that shouldn't be blocking) the implementation of the ERC20 side of things LGTM. I see that at least the happy path and some failure cases are covered in the tests, so should be good to go (at least for the audits)

Comment on lines +3 to +5
/* eslint prefer-const: "off" */
import { deployContractWithDeployer, getTransactionFees } from './util'
import hre, { ethers } from 'hardhat'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The husky prettier hook insists on making these changes 🤷

function lock(SupplySource memory supplySource, address from, uint256 value) internal {
if (supplySource.kind == SupplyKind.ERC20) {
IERC20 token = IERC20(supplySource.tokenAddress);
token.safeTransferFrom({from: from, to: address(this), value: value});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added soldocs in the IGateway interface.

Comment on lines +238 to +243
function test_propagation() public {
// TODO:
// 1. Test that propagation is rejected when sender is ERC20.
// 2. Test that propagation is rejected when receiver is ERC20.
// 3. Test that propagation is rejected when an intermediary subnet is ERC20.
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO, will open an issue to track these missing tests.

Aderyn alerted of this:

    Passing an arbitrary `from` address to `transferFrom` (or `safeTransferFrom`) can lead to loss of funds, because anyone can transfer tokens from the `from` address if an approval is made.

This is not a possibility since this parameter is in an internal library,
and the only caller passes in msg.sender. But I can't predict how this
code will evolve, so it may become a footgun down the line. Removing.
@raulk
Copy link
Contributor Author

raulk commented Dec 18, 2023

@snissn

  • assert fail funding a native subnet with erc20
  • assert fail funding an erc20 subnet with native
  • test using a real erc20 as opposed to a mock
  • test withdrawing from the subnet (is that possible?)

Implemented!

built hardhat entry points and makefile commands that execute code and use that as a test

Could you elaborate?

@raulk
Copy link
Contributor Author

raulk commented Dec 19, 2023

I'm seeing flaky tests failing at the diamond selector generation step, which is a Python script that calls Forge from within tests via FFI. It's flaky because it appears to fail occasionally, and with a different test each time.

I've raised a question in Slack to @dnkolegov to understand why we perform selector generation at test time, when we'll anyway need to do this during deployment or sooner.

On surface, it seems like a codegen thing at build time, so curious what other reasons led us to choose this solution.

@raulk
Copy link
Contributor Author

raulk commented Dec 19, 2023

For now, I'll go ahead and merge. dev seems to be broken anyway due to failed ABI generation and flaky tests.

@raulk raulk merged commit c008f0e into dev Dec 19, 2023
7 of 8 checks passed
@raulk raulk deleted the raulk/feat/erc20 branch December 19, 2023 00:19
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants