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

ERC-20 Withdrawals #312

Merged
merged 2 commits into from
Dec 10, 2024
Merged
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
17 changes: 12 additions & 5 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,9 @@ func (b *Builder) parseWithdrawalMessages(
}
for _, msg := range cosmosTx.GetBody().GetMessages() {
userWithdrawalMsgType := cdctypes.MsgTypeURL(new(rolluptypes.MsgInitiateWithdrawal))
erc20WithdrawalMsgType := cdctypes.MsgTypeURL(new(rolluptypes.MsgInitiateERC20Withdrawal))
feeWithdrawalMsgType := cdctypes.MsgTypeURL(new(rolluptypes.MsgInitiateFeeWithdrawal))
if msg.TypeUrl == userWithdrawalMsgType || msg.TypeUrl == feeWithdrawalMsgType {
if msg.TypeUrl == userWithdrawalMsgType || msg.TypeUrl == feeWithdrawalMsgType || msg.TypeUrl == erc20WithdrawalMsgType {
// Store the withdrawal message hash in the monomer EVM state db and populate the nonce in the event data.
err := b.storeWithdrawalMsgInEVM(execTxResult, ethState, header)
if err != nil {
Expand Down Expand Up @@ -362,11 +363,17 @@ func parseWithdrawalEventAttributes(withdrawalEvent *abcitypes.Event) (*crossdom
for _, attr := range withdrawalEvent.Attributes {
switch attr.Key {
case rolluptypes.AttributeKeySender:
senderCosmosAddress, err := sdk.AccAddressFromBech32(attr.Value)
if err != nil {
return nil, fmt.Errorf("convert sender to cosmos address: %v", err)
// check whether the sender address should be encoded as an Ethereum address or a Cosmos address
var sender common.Address
if common.IsHexAddress(attr.Value) {
sender = common.HexToAddress(attr.Value)
joshklop marked this conversation as resolved.
Show resolved Hide resolved
} else {
senderCosmosAddress, err := sdk.AccAddressFromBech32(attr.Value)
if err != nil {
return nil, fmt.Errorf("convert sender to cosmos address: %v", err)
}
sender = common.BytesToAddress(senderCosmosAddress.Bytes())
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved
}
sender := common.BytesToAddress(senderCosmosAddress.Bytes())
params.Sender = &sender
case rolluptypes.AttributeKeyL1Target:
target := common.HexToAddress(attr.Value)
Expand Down
6 changes: 4 additions & 2 deletions proto/rollup/v1/rollup.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ message Params {
string l1_fee_recipient = 1;
// L1 address of the cross-domain messenger contract.
string l1_cross_domain_messenger = 2;
// L1 address of the standard bridge contract.
string l1_standard_bridge = 3;
// Minimum amount of L2 fees that the FeeCollector account must have before they can be withdrawn.
uint64 min_fee_withdrawal_amount = 3;
uint64 min_fee_withdrawal_amount = 4;
// L1 gas limit for withdrawing fees to the L1 recipient address.
uint64 fee_withdrawal_gas_limit = 4;
uint64 fee_withdrawal_gas_limit = 5;
}

// L1BlockInfo represents information about an L1 block and associated L2 data.
Expand Down
33 changes: 31 additions & 2 deletions proto/rollup/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ service Msg {
// ApplyUserDeposit defines a method for applying a user deposit tx.
rpc ApplyUserDeposit(MsgApplyUserDeposit) returns (MsgApplyUserDepositResponse);

// InitiateWithdrawal defines a method for initiating a withdrawal from L2 to L1.
// InitiateWithdrawal defines a method for initiating an ETH withdrawal from L2 to L1.
rpc InitiateWithdrawal(MsgInitiateWithdrawal) returns (MsgInitiateWithdrawalResponse);

// InitiateERC20Withdrawal defines a method for initiating an ERC-20 withdrawal from L2 to L1.
rpc InitiateERC20Withdrawal(MsgInitiateERC20Withdrawal) returns (MsgInitiateERC20WithdrawalResponse);

// InitiateFeeWithdrawal defines a method for initiating a withdrawal of fees from L2 to the L1 fee recipient address.
rpc InitiateFeeWithdrawal(MsgInitiateFeeWithdrawal) returns (MsgInitiateFeeWithdrawalResponse);

Expand Down Expand Up @@ -54,7 +57,7 @@ message MsgApplyUserDeposit {
// MsgApplyUserDepositResponse defines the ApplyUserDeposit response type.
message MsgApplyUserDepositResponse {}

// MsgInitiateWithdrawal defines the message for initiating an L2 withdrawal.
// MsgInitiateWithdrawal defines the message for initiating an L2 ETH withdrawal.
message MsgInitiateWithdrawal {
option (cosmos.msg.v1.signer) = "sender";

Expand All @@ -78,6 +81,32 @@ message MsgInitiateWithdrawal {
// MsgInitiateWithdrawalResponse defines the Msg/InitiateWithdrawal response type.
message MsgInitiateWithdrawalResponse {}

// MsgInitiateERC20Withdrawal defines the message for initiating an L2 ERC-20 withdrawal.
message MsgInitiateERC20Withdrawal {
option (cosmos.msg.v1.signer) = "sender";

// The cosmos address of the user who wants to withdraw from L2.
string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// The ethereum address on L1 that the user wants to withdraw to.
string target = 2;
// The address of the ERC-20 token contract on L1.
string token_address = 3;
// The amount of ERC-20 tokens that the user wants to withdraw.
string value = 4 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
// Minimum gas limit for executing the message on L1.
bytes gas_limit = 5;
// Extra data to forward to L1 target.
bytes extra_data = 6;
}
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved

// MsgInitiateERC20WithdrawalResponse defines the Msg/InitiateERC20Withdrawal response type.
message MsgInitiateERC20WithdrawalResponse {}

// MsgInitiateFeeWithdrawal defines the message for initiating an L2 fee withdrawal to the L1 fee recipient address.
message MsgInitiateFeeWithdrawal {
option (cosmos.msg.v1.signer) = "sender";
Expand Down
2 changes: 1 addition & 1 deletion x/rollup/keeper/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func (k *Keeper) mintERC20(
amount sdkmath.Int,
) (*sdk.Event, error) {
// use the "erc20/{l1erc20addr}" format for the coin denom
coin := sdk.NewCoin("erc20/"+erc20addr[2:], amount)
coin := sdk.NewCoin(getERC20Denom(erc20addr), amount)
if err := k.bankkeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil {
return nil, fmt.Errorf("failed to mint ERC-20 deposit coins to the rollup module: %v", err)
}
Expand Down
6 changes: 6 additions & 0 deletions x/rollup/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,9 @@ func (k *Keeper) SetParams(ctx sdk.Context, params *types.Params) error { //noli
}
return nil
}

// getERC20Denom returns the Monomer L2 coin denom for the given ERC-20 L1 address.
// The "erc20/{l1erc20addr}" format is used for the L2 coin denom.
func getERC20Denom(erc20Addr string) string {
return "erc20/" + erc20Addr[2:]
}
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions x/rollup/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ func (s *KeeperTestSuite) setup() {
s.eventManger = sdkCtx.EventManager()
}

func (s *KeeperTestSuite) mockBurnETH() {
func (s *KeeperTestSuite) mockBurn() {
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
}

func (s *KeeperTestSuite) mockMintETH() {
func (s *KeeperTestSuite) mockMint() {
s.bankKeeper.EXPECT().MintCoins(s.ctx, types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.ctx, types.ModuleName, gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
}
Expand Down
97 changes: 96 additions & 1 deletion x/rollup/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import (
"context"
"fmt"
"math/big"
"strings"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
opbindings "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/polymerdao/monomer/x/rollup/types"
)
Expand Down Expand Up @@ -47,7 +52,7 @@ func (k *Keeper) InitiateWithdrawal(
msg *types.MsgInitiateWithdrawal,
) (*types.MsgInitiateWithdrawalResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
ctx.Logger().Debug("Withdrawing L2 assets", "sender", msg.Sender, "value", msg.Value)
ctx.Logger().Debug("Withdrawing ETH L2 assets", "sender", msg.Sender, "value", msg.Value)

cosmAddr, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
Expand Down Expand Up @@ -80,6 +85,96 @@ func (k *Keeper) InitiateWithdrawal(
return &types.MsgInitiateWithdrawalResponse{}, nil
}

func (k *Keeper) InitiateERC20Withdrawal(
goCtx context.Context,
msg *types.MsgInitiateERC20Withdrawal,
) (*types.MsgInitiateERC20WithdrawalResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
ctx.Logger().Debug("Withdrawing ERC-20 L2 assets", "sender", msg.Sender, "value", msg.Value, "tokenAddress", msg.TokenAddress)

cosmAddr, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
return nil, types.WrapError(types.ErrInvalidSender, "failed to create cosmos address for sender: %v; error: %v", msg.Sender, err)
}

if err = k.burnERC20(ctx, cosmAddr, msg.TokenAddress, msg.Value); err != nil {
return nil, types.WrapError(
types.ErrInitiateERC20Withdrawal,
"failed to burn ERC-20 for cosmosAddress: %v, tokenAddress: %v; err: %v",
cosmAddr,
msg.TokenAddress,
err,
)
}

params, err := k.GetParams(ctx)
if err != nil {
return nil, types.WrapError(types.ErrInitiateERC20Withdrawal, "failed to get params: %v", err)
}

// Pack the finalizeBridgeERC20 message to forward to the L1StandardBridge contract on Ethereum.
// see https://github.com/ethereum-optimism/optimism/blob/24a8d3e/packages/contracts-bedrock/src/universal/StandardBridge.sol#L267
standardBridgeABI, err := abi.JSON(strings.NewReader(opbindings.StandardBridgeMetaData.ABI))
if err != nil {
return nil, types.WrapError(types.ErrInitiateERC20Withdrawal, "failed to parse StandardBridge ABI: %v", err)
}
finalizeBridgeERC20Bz, err := standardBridgeABI.Pack(
"finalizeBridgeERC20",
common.HexToAddress(msg.TokenAddress), // local token
common.HexToAddress(msg.TokenAddress), // remote token
common.HexToAddress(msg.Sender), // from
common.HexToAddress(msg.Target), // to
msg.Value.BigInt(), // amount
msg.ExtraData, // extra data
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved
)
if err != nil {
return nil, types.WrapError(types.ErrInitiateERC20Withdrawal, "failed to pack finalizeBridgeERC20: %v", err)
}

// Pack the relayMessage to forward to the L1CrossDomainMessenger contract on Ethereum.
// see https://github.com/ethereum-optimism/optimism/blob/24a8d3e/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol#L207
crossDomainMessengerABI, err := abi.JSON(strings.NewReader(opbindings.CrossDomainMessengerMetaData.ABI))
if err != nil {
return nil, types.WrapError(types.ErrInitiateERC20Withdrawal, "failed to parse CrossDomainMessenger ABI: %v", err)
}
relayMessageBz, err := crossDomainMessengerABI.Pack(
"relayMessage",
big.NewInt(0), // nonce
common.HexToAddress(predeploys.L2StandardBridge), // sender
common.HexToAddress(params.L1StandardBridge), // target
big.NewInt(0), // value
big.NewInt(0), // min gas limit
finalizeBridgeERC20Bz, // message
)
if err != nil {
return nil, types.WrapError(types.ErrInitiateERC20Withdrawal, "failed to pack relayMessage: %v", err)
}

withdrawalValueHex := hexutil.EncodeBig(msg.Value.BigInt())
k.EmitEvents(ctx, sdk.Events{
sdk.NewEvent(
types.EventTypeWithdrawalInitiated,
// To forward the ERC-20 withdrawal to L1, we need to use the L2CrossDomainMessenger address as the msg.sender
sdk.NewAttribute(types.AttributeKeySender, predeploys.L2CrossDomainMessenger),
sdk.NewAttribute(types.AttributeKeyL1Target, params.L1CrossDomainMessenger),
// The ERC-20 withdrawal value is stored in the data field
sdk.NewAttribute(types.AttributeKeyValue, hexutil.EncodeBig(big.NewInt(0))),
sdk.NewAttribute(types.AttributeKeyGasLimit, hexutil.EncodeBig(new(big.Int).SetBytes(msg.GasLimit))),
sdk.NewAttribute(types.AttributeKeyData, hexutil.Encode(relayMessageBz)),
// The nonce attribute will be set by Monomer
),
sdk.NewEvent(
types.EventTypeBurnERC20,
sdk.NewAttribute(types.AttributeKeyL2WithdrawalTx, types.EventTypeWithdrawalInitiated),
sdk.NewAttribute(types.AttributeKeyFromCosmosAddress, msg.Sender),
sdk.NewAttribute(types.AttributeKeyERC20Address, msg.TokenAddress),
sdk.NewAttribute(types.AttributeKeyValue, withdrawalValueHex),
),
})

return &types.MsgInitiateERC20WithdrawalResponse{}, nil
}
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved

func (k *Keeper) InitiateFeeWithdrawal(
goCtx context.Context,
_ *types.MsgInitiateFeeWithdrawal,
Expand Down
80 changes: 76 additions & 4 deletions x/rollup/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (s *KeeperTestSuite) TestApplyUserDeposit() {
if test.setupMocks != nil {
test.setupMocks()
}
s.mockMintETH()
s.mockMint()

resp, err := s.rollupKeeper.ApplyUserDeposit(s.ctx, &types.MsgApplyUserDeposit{
Tx: test.txBytes,
Expand All @@ -131,6 +131,7 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() {
l1Target := "0x12345abcde"
withdrawalAmount := math.NewInt(1000000)

//nolint:dupl
tests := map[string]struct {
sender string
setupMocks func()
Expand All @@ -146,14 +147,14 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() {
},
"bank keeper insufficient funds failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(types.ErrBurnETH)
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(types.ErrBurnETH).AnyTimes()
natebeauregard marked this conversation as resolved.
Show resolved Hide resolved
},
sender: sender,
shouldError: true,
},
"bank keeper burn coins failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest)
s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest).AnyTimes()
},
sender: sender,
shouldError: true,
Expand All @@ -165,7 +166,7 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() {
if test.setupMocks != nil {
test.setupMocks()
}
s.mockBurnETH()
s.mockBurn()

resp, err := s.rollupKeeper.InitiateWithdrawal(s.ctx, &types.MsgInitiateWithdrawal{
Sender: test.sender,
Expand Down Expand Up @@ -194,6 +195,77 @@ func (s *KeeperTestSuite) TestInitiateWithdrawal() {
}
}

func (s *KeeperTestSuite) TestInitiateERC20Withdrawal() {
sender := sdk.AccAddress("addr").String()
l1Target := "0x12345abcde"
erc20TokenAddress := "0x0123456789abcdef"
withdrawalAmount := math.NewInt(1000000)

//nolint:dupl
tests := map[string]struct {
sender string
setupMocks func()
shouldError bool
}{
"successful message": {
sender: sender,
shouldError: false,
},
"invalid sender addr": {
sender: "invalid",
shouldError: true,
},
"bank keeper insufficient funds failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(s.ctx, gomock.Any(), types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest).AnyTimes()
},
sender: sender,
shouldError: true,
},
"bank keeper burn coins failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().BurnCoins(s.ctx, types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest).AnyTimes()
},
sender: sender,
shouldError: true,
},
}

for name, test := range tests {
s.Run(name, func() {
if test.setupMocks != nil {
test.setupMocks()
}
s.mockBurn()

resp, err := s.rollupKeeper.InitiateERC20Withdrawal(s.ctx, &types.MsgInitiateERC20Withdrawal{
Sender: test.sender,
Target: l1Target,
TokenAddress: erc20TokenAddress,
Value: withdrawalAmount,
})

if test.shouldError {
s.Require().Error(err)
s.Require().Nil(resp)
} else {
s.Require().NoError(err)
s.Require().NotNil(resp)

// Verify that the expected event types are emitted
expectedEventTypes := []string{
sdk.EventTypeMessage,
types.EventTypeWithdrawalInitiated,
types.EventTypeBurnERC20,
}
for i, event := range s.eventManger.Events() {
s.Require().Equal(expectedEventTypes[i], event.Type)
}
}
})
}
}

func (s *KeeperTestSuite) TestInitiateFeeWithdrawal() {
tests := map[string]struct {
setupMocks func()
Expand Down
Loading
Loading