Skip to content

Commit

Permalink
feat(withdrawer): bridged ERC20 token withdrawals (#1149)
Browse files Browse the repository at this point in the history
## Summary
implement withdrawals of ERC20 tokens that are of type
`AstriaBridgeableERC20` (see implemented contract).

## Background
we want to be able to withdraw ERC20 tokens that are bridged to a
rollup.

## Changes
- implement `AstriaBridgeableERC20` which is a standard `ERC20` contract
with additional functionality for minting (not used by the withdrawer,
implemented/tested here
astriaorg/astria-geth#20) as well as
functionality for withdrawing
- implement `IAstriaWithdrawer` which is implemented by both
`AstriaWithdrawer` and `AstriaMintableERC20`.
- the withdrawer now interacts with a `IAstriaWithdrawer`. both native
assets and ERC20 withdrawals have the same event signatures, so no
additional code was needed for the withdrawer itself.
- to use the withdrawer with an `AstriaBridgeableERC20`, the
`ASTRIA_BRIDGE_WITHDRAWER_ETHEREUM_CONTRACT_ADDRESS` is set to some
`AstriaMintableERC20`, and
`ASTRIA_BRIDGE_WITHDRAWER_ROLLUP_ASSET_DENOMINATION` is set to the
rollup asset's denomination as represented on the sequencer. for
example, if the asset is represented on the sequencer is
`transfer/channel-1/usdc`, that is the rollup asset denomination. the
`name/symbol` of the ERC20 contract are not relevant.
- also update the build script to write the generated abigen contract
bindings to files, this is easier for debugging and unit testing.

## Testing
unit tests

## Related Issues

closes #924
  • Loading branch information
noot authored Jun 7, 2024
1 parent e3595cb commit b52a824
Show file tree
Hide file tree
Showing 22 changed files with 3,177 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
crates/astria-core/src/generated/** linguist-generated=true
crates/astria-bridge-withdrawer/src/withdrawer/ethereum/generated/** linguist-generated=true
crates/astria-bridge-withdrawer/ethereum/out/** linguist-generated=true
specs/** linguist-documentation=true
7 changes: 0 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,11 @@ jobs:
with:
version: "24.4"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install python and solc
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run tests
timeout-minutes: 20
run: |
pip install solc-select
solc-select install 0.8.21
solc-select use 0.8.21
cargo nextest run --package astria-bridge-withdrawer -- --include-ignored
doctest:
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "crates/astria-bridge-withdrawer/ethereum/lib/forge-std"]
path = crates/astria-bridge-withdrawer/ethereum/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "crates/astria-bridge-withdrawer/ethereum/lib/openzeppelin-contracts"]
path = crates/astria-bridge-withdrawer/ethereum/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions crates/astria-bridge-withdrawer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ wiremock = { workspace = true }

[build-dependencies]
astria-build-info = { path = "../astria-build-info", features = ["build"] }
ethers = { workspace = true }
28 changes: 28 additions & 0 deletions crates/astria-bridge-withdrawer/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
use ethers::contract::Abigen;

fn main() -> Result<(), Box<dyn std::error::Error>> {
astria_build_info::emit("bridge-withdrawer-v")?;

println!("cargo:rerun-if-changed=ethereum/src/AstriaWithdrawer.sol");
println!("cargo:rerun-if-changed=ethereum/src/IAstriaWithdrawer.sol");
println!("cargo:rerun-if-changed=ethereum/src/AstriaBridgeableERC20.sol");

Abigen::new(
"IAstriaWithdrawer",
"./ethereum/out/IAstriaWithdrawer.sol/IAstriaWithdrawer.json",
)?
.generate()?
.write_to_file("./src/withdrawer/ethereum/generated/astria_withdrawer_interface.rs")?;

Abigen::new(
"AstriaWithdrawer",
"./ethereum/out/AstriaWithdrawer.sol/AstriaWithdrawer.json",
)?
.generate()?
.write_to_file("./src/withdrawer/ethereum/generated/astria_withdrawer.rs")?;

Abigen::new(
"AstriaBridgeableERC20",
"./ethereum/out/AstriaBridgeableERC20.sol/AstriaBridgeableERC20.json",
)?
.generate()?
.write_to_file("./src/withdrawer/ethereum/generated/astria_bridgeable_erc20.rs")?;

Ok(())
}
Submodule openzeppelin-contracts added at dbb610

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.21;

import {IAstriaWithdrawer} from "./IAstriaWithdrawer.sol";
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract AstriaBridgeableERC20 is IAstriaWithdrawer, ERC20 {
// the `astriaBridgeSenderAddress` built into the astria-geth node
address public immutable BRIDGE;

// the divisor used to convert the rollup asset amount to the base chain denomination
//
// set to 10 ** (TOKEN_DECIMALS - BASE_CHAIN_ASSET_PRECISION) on contract creation
uint256 private immutable DIVISOR;

// emitted when tokens are minted from a deposit
event Mint(address indexed account, uint256 amount);

modifier onlyBridge() {
require(msg.sender == BRIDGE, "AstriaBridgeableERC20: only bridge can mint");
_;
}

constructor(
address _bridge,
uint32 _baseChainAssetPrecision,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol) {
uint8 decimals = decimals();
if (_baseChainAssetPrecision > decimals) {
revert("AstriaBridgeableERC20: base chain asset precision must be less than or equal to token decimals");
}

BASE_CHAIN_ASSET_PRECISION = _baseChainAssetPrecision;
DIVISOR = 10 ** (decimals - _baseChainAssetPrecision);
BRIDGE = _bridge;
}

modifier sufficientValue(uint256 amount) {
require(amount / DIVISOR > 0, "AstriaBridgeableERC20: insufficient value, must be greater than 10 ** (TOKEN_DECIMALS - BASE_CHAIN_ASSET_PRECISION)");
_;
}

function mint(address _to, uint256 _amount)
external
onlyBridge
{
_mint(_to, _amount);
emit Mint(_to, _amount);
}

function withdrawToSequencer(uint256 _amount, address _destinationChainAddress)
external
sufficientValue(_amount)
{
_burn(msg.sender, _amount);
emit SequencerWithdrawal(msg.sender, _amount, _destinationChainAddress);
}

function withdrawToIbcChain(uint256 _amount, string calldata _destinationChainAddress, string calldata _memo)
external
sufficientValue(_amount)
{
_burn(msg.sender, _amount);
emit Ics20Withdrawal(msg.sender, _amount, _destinationChainAddress, _memo);
}
}
27 changes: 4 additions & 23 deletions crates/astria-bridge-withdrawer/ethereum/src/AstriaWithdrawer.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.21;

import {IAstriaWithdrawer} from "./IAstriaWithdrawer.sol";

// This contract facilitates withdrawals of the native asset from the rollup to the base chain.
//
// Funds can be withdrawn to either the sequencer or the origin chain via IBC.
contract AstriaWithdrawer {
// the precision of the asset on the base chain.
//
// the amount transferred on the base chain will be divided by 10 ^ (18 - BASE_CHAIN_ASSET_PRECISION).
//
// for example, if base chain asset is precision is 6, the divisor would be 10^12.
uint32 public immutable BASE_CHAIN_ASSET_PRECISION;

contract AstriaWithdrawer is IAstriaWithdrawer {
// the divisor used to convert the rollup asset amount to the base chain denomination
//
// set to 10^ASSET_WITHDRAWAL_DECIMALS on contract creation
// set to 10 ** (18 - BASE_CHAIN_ASSET_PRECISION) on contract creation
uint256 private immutable DIVISOR;

constructor(uint32 _baseChainAssetPrecision) {
Expand All @@ -25,20 +20,6 @@ contract AstriaWithdrawer {
DIVISOR = 10 ** (18 - _baseChainAssetPrecision);
}

// emitted when a withdrawal to the sequencer is initiated
//
// the `sender` is the evm address that initiated the withdrawal
// the `destinationChainAddress` is the address on the sequencer the funds will be sent to
event SequencerWithdrawal(address indexed sender, uint256 indexed amount, address destinationChainAddress);

// emitted when a withdrawal to the origin chain is initiated.
// the withdrawal is sent to the origin chain via IBC from the sequencer using the denomination trace.
//
// the `sender` is the evm address that initiated the withdrawal
// the `destinationChainAddress` is the address on the origin chain the funds will be sent to
// the `memo` is an optional field that will be used as the ICS20 packet memo
event Ics20Withdrawal(address indexed sender, uint256 indexed amount, string destinationChainAddress, string memo);

modifier sufficientValue(uint256 amount) {
require(amount / DIVISOR > 0, "AstriaWithdrawer: insufficient value, must be greater than 10 ** (18 - BASE_CHAIN_ASSET_PRECISION)");
_;
Expand Down
25 changes: 25 additions & 0 deletions crates/astria-bridge-withdrawer/ethereum/src/IAstriaWithdrawer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.21;

abstract contract IAstriaWithdrawer {
// the precision of the asset on the base chain.
//
// the amount transferred on the base chain will be divided by 10 ^ (18 - BASE_CHAIN_ASSET_PRECISION).
//
// for example, if base chain asset is precision is 6, the divisor would be 10^12.
uint32 public immutable BASE_CHAIN_ASSET_PRECISION;

// emitted when a withdrawal to the sequencer is initiated
//
// the `sender` is the evm address that initiated the withdrawal
// the `destinationChainAddress` is the address on the sequencer the funds will be sent to
event SequencerWithdrawal(address indexed sender, uint256 indexed amount, address destinationChainAddress);

// emitted when a withdrawal to the IBC origin chain is initiated.
// the withdrawal is sent to the origin chain via IBC from the sequencer using the denomination trace.
//
// the `sender` is the evm address that initiated the withdrawal
// the `destinationChainAddress` is the address on the origin chain the funds will be sent to
// the `memo` is an optional field that will be used as the ICS20 packet memo
event Ics20Withdrawal(address indexed sender, uint256 indexed amount, string destinationChainAddress, string memo);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use serde::{
Serialize,
};

use crate::withdrawer::ethereum::astria_withdrawer::{
use crate::withdrawer::ethereum::astria_withdrawer_interface::{
Ics20WithdrawalFilter,
SequencerWithdrawalFilter,
};
Expand Down Expand Up @@ -182,7 +182,7 @@ fn calculate_packet_timeout_time(timeout_delta: Duration) -> eyre::Result<u64> {
#[cfg(test)]
mod tests {
use super::*;
use crate::withdrawer::ethereum::astria_withdrawer::SequencerWithdrawalFilter;
use crate::withdrawer::ethereum::astria_withdrawer_interface::SequencerWithdrawalFilter;

#[test]
fn event_to_bridge_unlock() {
Expand Down
Loading

0 comments on commit b52a824

Please sign in to comment.