Skip to content
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
*.sol linguist-language=Solidity

contracts/astria_bridgeable_erc20.go linguist-generated=true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ profile.cov
logs/

tests/spec-tests/

contracts/abi
contracts/bin
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
path = tests/evm-benchmarks
url = https://github.com/ipsilon/evm-benchmarks
shallow = true
[submodule "contracts/astria-bridge-contracts"]
path = contracts/astria-bridge-contracts
url = https://github.com/astriaorg/astria-bridge-contracts.git
2 changes: 1 addition & 1 deletion cmd/evm/internal/t8ntool/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func Transaction(ctx *cli.Context) error {
}
// Check intrinsic gas
if gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil,
chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int), 0)); err != nil {
chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int), 0), false); err != nil {
r.Error = err
results = append(results, r)
continue
Expand Down
95 changes: 95 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# astria bridgeable erc20s

Package for the `AstriaBridgeableERC20` contract.

## Initializing

Requirements:

- foundry

Build:

```sh
git submodule update --init --recursive
cd astria-bridge-contracts
forge build
```

## Go bindings

If you change the contract and wish to update the go bindings, run:

```sh
chmod +x
./generate-bindings.sh
```

## Testing

To test the full end-to-end flow, run the sequencer, cometbft, composer, and conductor. Ensure the configured chain IDs are correct.

Copy the example .env:

```sh
cp local.env.example .env && source .env
```

Deploy `AstriaBridgeableERC20.sol`:

```sh
forge script script/AstriaBridgeableERC20.s.sol:AstriaBridgeableERC20Script \
--rpc-url $RPC_URL --broadcast --sig "deploy()" -vvvv
```

Take note of the deployed address.

Add the following to the genesis file under `astriaBridgeAddresses`:

```json
"astriaBridgeAddresses": [
{
"bridgeAddress": "0x1c0c490f1b5528d8173c5de46d131160e4b2c0c3",
"startHeight": 1,
"assetDenom": "nria",
"assetPrecision": 6,
"erc20asset": {
"contractAddress":"0x9Aae647A1CB2ec6b39afd552aD149F6A26Bb2aD6",
"contractPrecision": 18
}
}
],
```

Note: this mints `nria` as an erc20 instead of the native asset.

`bridgeAddress` is the bridge address that corresponds to this asset on the sequencer chain.
`assetDenom` does not need to match the name of the token in the deployed contract, but it does need to match the denom of the token on the sequencer.
`contractAddress` in `erc20asset` is the address of the contract deployed above.

Stop the geth node and rerun `geth init --genesis genesis.json`. Restart the node. The contract is now initialized as a bridge from the sequencer.

Run the following with the `astria-cli`:

```sh
# this matches the `bridgeAddress` 0x1c0c490f1b5528d8173c5de46d131160e4b2c0c3 in the genesis above
export SEQUENCER_PRIVATE_KEY=2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90
./target/debug/astria-cli sequencer init-bridge-account --sequencer-url=http://localhost:26657 --rollup-name=astria
# the `destination-chain-address` matches the `PRIVATE_KEY` in local.example.env
./target/debug/astria-cli sequencer bridge-lock --sequencer-url=http://localhost:26657 --amount=1000000 --destination-chain-address=0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102 --sequencer.chain-id=astria -- 1c0c490f1b5528d8173c5de46d131160e4b2c0c3
```

This initializes the bridge account and also transfer funds over.

Check your ERC20 balance:

```sh
forge script script/AstriaBridgeableERC20.s.sol:AstriaBridgeableERC20Script \
--rpc-url $RPC_URL --sig "getBalance()" -vvvv
```

If everything worked, you should see a balance logged:
```
== Logs ==
1000000000000000000
```
1 change: 1 addition & 0 deletions contracts/astria-bridge-contracts
1,401 changes: 1,401 additions & 0 deletions contracts/astria_bridgeable_erc20.go

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions contracts/generate-bindings.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
solc --optimize --optimize-runs=200 \
--metadata --metadata-literal \
--base-path "astria-bridge-contracts" \
--abi "astria-bridge-contracts/src/AstriaBridgeableERC20.sol" \
-o abi/ --overwrite

solc --optimize --optimize-runs=200 \
--base-path "astria-bridge-contracts" \
--bin "astria-bridge-contracts/src/AstriaBridgeableERC20.sol" \
-o bin/ --overwrite

abigen --abi abi/AstriaBridgeableERC20.abi --bin bin/AstriaBridgeableERC20.bin --pkg contracts --type AstriaBridgeableERC20 --out astria_bridgeable_erc20.go
2 changes: 1 addition & 1 deletion core/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
return func(i int, gen *BlockGen) {
toaddr := common.Address{}
data := make([]byte, nbytes)
gas, _ := IntrinsicGas(data, nil, false, false, false, false)
gas, _ := IntrinsicGas(data, nil, false, false, false, false, false)
signer := gen.Signer()
gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil {
Expand Down
40 changes: 35 additions & 5 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
cmath "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
)

Expand Down Expand Up @@ -65,7 +66,12 @@ func (result *ExecutionResult) Revert() []byte {
}

// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation bool, isHomestead, isEIP2028 bool, isEIP3860 bool) (uint64, error) {
func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation bool, isHomestead, isEIP2028 bool, isEIP3860 bool, isDepositTx bool) (uint64, error) {
if isDepositTx {
// deposit txs are gasless
return 0, nil
}

// Set the starting gas for the raw transaction
var gas uint64
if isContractCreation && isHomestead {
Expand Down Expand Up @@ -274,6 +280,12 @@ func (st *StateTransition) buyGas() error {
}

func (st *StateTransition) preCheck() error {
if st.msg.IsDepositTx {
// deposit txs do not require checks as they are part of rollup consensus,
// not txs that originate externally.
return nil
}

// Only check transactions that are not fake
msg := st.msg
if !msg.SkipAccountChecks {
Expand Down Expand Up @@ -362,16 +374,23 @@ func (st *StateTransition) preCheck() error {
// However if any consensus issue encountered, return the error directly with
// nil evm execution result.
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
// if this is a deposit tx, we only need to mint funds and no gas is used.
if st.msg.IsDepositTx {
st.state.AddBalance(st.msg.From, st.msg.Value)
// if this is a native asset deposit tx, we only need to mint funds.
if st.msg.IsDepositTx && len(st.msg.Data) == 0 {
log.Debug("deposit tx minting funds", "to", *st.msg.To, "value", st.msg.Value)
st.state.AddBalance(*st.msg.To, st.msg.Value)
return &ExecutionResult{
UsedGas: 0,
Err: nil,
ReturnData: nil,
}, nil
}

if st.msg.IsDepositTx {
st.initialGas = st.msg.GasLimit
st.gasRemaining = st.msg.GasLimit
log.Debug("deposit tx minting erc20", "to", *st.msg.To, "value", st.msg.Value)
}

// First check this message satisfies all consensus rules before
// applying the message. The rules include these clauses
//
Expand Down Expand Up @@ -402,7 +421,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
)

// Check clauses 4-5, subtract intrinsic gas if everything is correct
gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, msg.IsDepositTx)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -438,6 +457,17 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
}

// if this is a deposit tx, don't refund gas and also don't pay to the coinbase,
// as no gas was used.
if st.msg.IsDepositTx {
log.Debug("deposit tx executed", "to", *st.msg.To, "value", st.msg.Value, "from", st.msg.From, "gasUsed", st.gasUsed(), "err", vmerr)
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}

if !rules.IsLondon {
// Before EIP-3529: refunds were capped to gasUsed / 2
st.refundGas(params.RefundQuotient)
Expand Down
2 changes: 1 addition & 1 deletion core/txpool/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, opts.Config.IsIstanbul(head.Number), opts.Config.IsShanghai(head.Number, head.Time))
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil, true, opts.Config.IsIstanbul(head.Number), opts.Config.IsShanghai(head.Number, head.Time), false)
if err != nil {
return err
}
Expand Down
24 changes: 20 additions & 4 deletions core/types/deposit_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,53 @@ import (
var _ TxData = &DepositTx{}

type DepositTx struct {
// the address of the account that initiated the deposit
// the bridge sender address set in the genesis file
// ie. the minter or the caller of the ERC20 contract
From common.Address
// value to be minted to `From`
// value to be minted to the recipient, if this is a native asset mint
Value *big.Int
// gas limit
Gas uint64
// if this is a native asset mint, this is set to the mint recipient
// if this is an ERC20 mint, this is set to the ERC20 contract address
To *common.Address
// if this is an ERC20 mint, the following field is set
// to the `mint` function calldata.
Data []byte
}

func (tx *DepositTx) copy() TxData {
to := new(common.Address)
if tx.To != nil {
*to = *tx.To
}

cpy := &DepositTx{
From: tx.From,
Value: new(big.Int),
Gas: tx.Gas,
To: to,
Data: make([]byte, len(tx.Data)),
}

if tx.Value != nil {
cpy.Value.Set(tx.Value)
}
copy(cpy.Data, tx.Data)
return cpy
}

func (tx *DepositTx) txType() byte { return DepositTxType }
func (tx *DepositTx) chainID() *big.Int { return common.Big0 }
func (tx *DepositTx) accessList() AccessList { return nil }
func (tx *DepositTx) data() []byte { return nil }
func (tx *DepositTx) data() []byte { return tx.Data }
func (tx *DepositTx) gas() uint64 { return tx.Gas }
func (tx *DepositTx) gasFeeCap() *big.Int { return new(big.Int) }
func (tx *DepositTx) gasTipCap() *big.Int { return new(big.Int) }
func (tx *DepositTx) gasPrice() *big.Int { return new(big.Int) }
func (tx *DepositTx) value() *big.Int { return tx.Value }
func (tx *DepositTx) nonce() uint64 { return 0 }
func (tx *DepositTx) to() *common.Address { return nil }
func (tx *DepositTx) to() *common.Address { return tx.To }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how were we identifying the correct account to send funds to previously?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

using the From field, but i felt that was confusing, and now we need to set the From field to the bridge "account"


func (tx *DepositTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
return dst.Set(new(big.Int))
Expand Down
11 changes: 11 additions & 0 deletions genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@
"startHeight": 1,
"assetDenom": "nria",
"assetPrecision": 9
},
{
"bridgeAddress": "34fec43c7fcab9aef3b3cf8aba855e41ee69ca3a",
"startHeight": 1,
"assetDenom": "transfer/channel-1/usdc",
"assetPrecision": 6,
"erc20asset": {
"contractAddress":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"contractPrecision": 6
}
}
],
"astriaBridgeSenderAddress": "0x0000000000000000000000000000000000000000",
"astriaFeeCollectors": {
"1": "0xaC21B97d35Bf75A7dAb16f35b111a50e78A72F30"
},
Expand Down
27 changes: 21 additions & 6 deletions grpc/execution/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ type ExecutionServiceServerV1Alpha2 struct {

bridgeAddresses map[string]*params.AstriaBridgeAddressConfig // astria bridge addess to config for that bridge account
bridgeAllowedAssetIDs map[[32]byte]struct{} // a set of allowed asset IDs structs are left empty
nextFeeRecipient common.Address // Fee recipient for the next block
bridgeSenderAddress common.Address // address from which AstriaBridgeableERC20 contracts are called

nextFeeRecipient common.Address // Fee recipient for the next block
}

var (
Expand Down Expand Up @@ -104,16 +106,25 @@ func NewExecutionServiceServerV1Alpha2(eth *eth.Ethereum) (*ExecutionServiceServ
return nil, fmt.Errorf("invalid bridge address config: %w", err)
}

if cfg.Erc20Asset != nil && nativeBridgeSeen {
return nil, errors.New("only one native bridge address is allowed")
}
if cfg.Erc20Asset != nil && !nativeBridgeSeen {
if cfg.Erc20Asset == nil {
if nativeBridgeSeen {
return nil, errors.New("only one native bridge address is allowed")
}
nativeBridgeSeen = true
}

if cfg.Erc20Asset != nil && bc.Config().AstriaBridgeSenderAddress == (common.Address{}) {
return nil, errors.New("astria bridge sender address must be set for bridged ERC20 assets")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Noted further down, but we should probably use string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AstriaBridgeSenderAddress is an evm address address (the fake address mints are sent from) so can leave this

}

bridgeAddresses[string(cfg.BridgeAddress)] = &cfg
assetID := sha256.Sum256([]byte(cfg.AssetDenom))
bridgeAllowedAssetIDs[assetID] = struct{}{}
if cfg.Erc20Asset == nil {
log.Info("bridge for sequencer native asset initialized", "bridgeAddress", cfg.BridgeAddress, "assetDenom", cfg.AssetDenom)
} else {
log.Info("bridge for ERC20 asset initialized", "bridgeAddress", cfg.BridgeAddress, "assetDenom", cfg.AssetDenom, "contractAddress", cfg.Erc20Asset.ContractAddress)
}
}
}

Expand Down Expand Up @@ -142,6 +153,7 @@ func NewExecutionServiceServerV1Alpha2(eth *eth.Ethereum) (*ExecutionServiceServ
bc: bc,
bridgeAddresses: bridgeAddresses,
bridgeAllowedAssetIDs: bridgeAllowedAssetIDs,
bridgeSenderAddress: bc.Config().AstriaBridgeSenderAddress,
nextFeeRecipient: nextFeeRecipient,
}, nil
}
Expand Down Expand Up @@ -237,9 +249,12 @@ func (s *ExecutionServiceServerV1Alpha2) ExecuteBlock(ctx context.Context, req *
return nil, status.Error(codes.FailedPrecondition, "Block can only be created on top of soft block.")
}

// the height that this block will be at
height := s.bc.CurrentBlock().Number.Uint64() + 1

txsToProcess := types.Transactions{}
for _, tx := range req.Transactions {
unmarshalledTx, err := validateAndUnmarshalSequencerTx(tx, s.bridgeAddresses, s.bridgeAllowedAssetIDs)
unmarshalledTx, err := validateAndUnmarshalSequencerTx(height, tx, s.bridgeAddresses, s.bridgeAllowedAssetIDs, s.bridgeSenderAddress)
if err != nil {
log.Debug("failed to validate sequencer tx, ignoring", "tx", tx, "err", err)
continue
Expand Down
Loading