This Vyper smart contract facilitates the purchase and sale of ETFs (Exchange-Traded Funds) on the Ethereum blockchain. It allows users to buy and sell ETFs using various assets, including Ether and ERC-20 tokens, with integration to the Compass cross-chain bridge for ETF token transfers.
ASSET
: Immutable address of the primary asset (e.g., USDT)ASSET_DECIMALS_NUMERATOR
: Conversion factor for asset decimalsROUTER02
: Uniswap V3 Router02 address for token swapsWETH9
: WETH contract addressrefund_wallet
: Address to receive refunds and depositscompass_evm
: Compass bridge contract addresspaloma
: Cross-chain identifier (bytes32)fee
: Fee amount for ETF operationsfee_receiver
: Address to receive fees
- ERC20: Standard ERC-20 token interface
- SwapRouter02: Uniswap V3 router for token swaps
- Weth: Wrapped Ether contract
- Compass: Cross-chain bridge contract
@deploy
def __init__(_router: address, _initial_asset: address, _refund_wallet: address, _compass_evm: address, _fee: uint256, _fee_receiver: address)
Purpose: Initializes the contract with core parameters.
Parameters:
_router
: Uniswap V3 Router02 address_initial_asset
: Primary asset address (e.g., USDT)_refund_wallet
: Wallet to receive deposits and refunds_compass_evm
: Compass bridge contract address_fee
: Fee amount for ETF operations_fee_receiver
: Address to receive fees
Security Considerations:
- Validates router address and retrieves WETH9 address
- Calculates asset decimal conversion factor
- Emits initialization events for transparency
Usage Example:
# Deploy with USDT as primary asset
purchaser = project.purchaser.deploy(
router="0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45",
initial_asset="0xdAC17F958D2ee523a2206206994597C13D831ec7", # USDT
refund_wallet="0xCdE7fB746AF9C308F10D1df56caF45ac3048653c",
compass="0x71956340a586db3afD10C2645Dbe8d065dD79AC8",
fee=0,
fee_address="0x7C303D43aDF7055ff3Ef88c525803D3ABBDD2860"
)
Purpose: Updates the Compass bridge contract address.
Access Control: Only callable by current compass contract Security: Validates SLC switch is unavailable before update
Usage Example:
# Only callable by current compass contract
purchaser.update_compass(new_compass_address, sender=compass_contract)
Purpose: Updates the refund wallet address.
Access Control: Requires Paloma validation Security: Validates new address is not zero
Usage Example:
# Requires Paloma validation
purchaser.update_refund_wallet(new_wallet, sender=compass_contract)
Purpose: Updates the fee amount for ETF operations.
Access Control: Requires Paloma validation Security: Validates fee is non-negative
Usage Example:
# Requires Paloma validation
purchaser.update_fee(1000000, sender=compass_contract) # 0.001 ETH
Purpose: Updates the fee receiver address.
Access Control: Requires Paloma validation Security: Validates new address is not zero
Usage Example:
# Requires Paloma validation
purchaser.update_fee_receiver(new_receiver, sender=compass_contract)
Purpose: Sets the Paloma cross-chain identifier.
Access Control: Only callable by compass contract when paloma is not set Security: Validates message data length and sender
Usage Example:
# Only callable once by compass contract
purchaser.set_paloma(sender=compass_contract)
Purpose: Creates a new single ETF with specified parameters.
Parameters:
_token_name
: ETF token name (max 64 chars)_token_symbol
: ETF token symbol (max 32 chars)_token_description
: ETF description (max 256 chars)_etf_ticker
: ETF ticker symbol (max 40 chars)_expense_ratio
: Expense ratio (max 1,000,000 = 100%)
Security Considerations:
- Validates all string parameters are non-empty
- Validates expense ratio ≤ 1,000,000
- Requires fee payment if fee > 0
- Refunds excess ETH sent
Usage Example:
# Create single ETF with 0.5% expense ratio
purchaser.create_signle_etf(
"Bitcoin ETF",
"BTCETF",
"Bitcoin Exchange Traded Fund",
"BTCETF",
5000, # 0.5%
value=0, # No fee required
sender=user
)
register_single_etf(_etf_token_denom, _etf_token_name, _etf_token_symbol, _etf_token_description, _etf_ticker, _expense_ratio)
Purpose: Registers an existing single ETF token.
Parameters:
_etf_token_denom
: ETF token denomination (max 96 chars)_etf_token_name
: ETF token name (max 64 chars)_etf_token_symbol
: ETF token symbol (max 32 chars)_etf_token_description
: ETF description (max 256 chars)_etf_ticker
: ETF ticker symbol (max 40 chars)_expense_ratio
: Expense ratio (max 1,000,000 = 100%)
Security Considerations:
- Same validation as create_single_etf
- Used for registering pre-existing tokens
Usage Example:
# Register existing single ETF
purchaser.register_single_etf(
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/TICKER.123",
"Bitcoin ETF",
"BTCETF",
"Bitcoin Exchange Traded Fund",
"BTCETF",
5000, # 0.5%
value=0,
sender=user
)
create_composite_etf(_etf_token_name, _etf_token_symbol, _etf_token_description, _expense_ratio, _token0_denom, _token0_position, _token1_denom, _token1_position)
Purpose: Creates a new composite ETF with two underlying tokens.
Parameters:
_etf_token_name
: ETF token name (max 64 chars)_etf_token_symbol
: ETF token symbol (max 32 chars)_etf_token_description
: ETF description (max 256 chars)_expense_ratio
: Expense ratio (max 1,000,000 = 100%)_token0_denom
: First token denomination (max 96 chars)_token0_position
: First token position percentage_token1_denom
: Second token denomination (max 96 chars)_token1_position
: Second token position percentage
Security Considerations:
- Validates all string parameters are non-empty
- Validates expense ratio ≤ 1,000,000
- Validates token positions > 0
- Validates token0 ≠ token1
- Validates positions sum to 100
- Requires fee payment if fee > 0
Usage Example:
# Create 60/40 BTC/ETH composite ETF
purchaser.create_composite_etf(
"Bitcoin Ethereum ETF",
"BTCETH",
"60% Bitcoin, 40% Ethereum ETF",
3000, # 0.3%
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/BTC.123",
60,
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/ETH.456",
40,
value=0,
sender=user
)
register_composite_etf(_etf_token_denom, _etf_token_name, _etf_token_symbol, _etf_token_description, _expense_ratio, _token0_denom, _token0_position, _token1_denom, _token1_position)
Purpose: Registers an existing composite ETF token.
Parameters: Same as create_composite_etf plus _etf_token_denom
Security Considerations:
- Same validation as create_composite_etf
- Used for registering pre-existing composite tokens
Usage Example:
# Register existing composite ETF
purchaser.register_composite_etf(
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/BTCETH.123",
"Bitcoin Ethereum ETF",
"BTCETH",
"60% Bitcoin, 40% Ethereum ETF",
3000,
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/BTC.123",
60,
"factory/paloma1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7pv/ETH.456",
40,
value=0,
sender=user
)
Purpose: Purchases ETF tokens using specified input asset.
Parameters:
_etf_token
: ETF token address to purchase_etf_amount
: Amount of ETF tokens to purchase_amount_in
: Amount of input asset to spend_recipient
: Address to receive ETF tokens_path
: Uniswap swap path (optional)_min_amount
: Minimum output amount (optional)
Security Considerations:
- Validates all addresses are non-zero
- Validates amounts > 0
- Requires Paloma and refund wallet to be set
- Handles direct asset transfers and swaps via Uniswap
- Supports ETH/WETH conversions
- Applies decimal conversion for USD amount calculation
- Refunds excess ETH
Usage Example:
# Buy ETF with USDT directly
purchaser.buy(
etf_token="0x123...",
etf_amount=1000000, # 1 ETF token
amount_in=50000000, # 50 USDT
recipient=user.address,
sender=user
)
# Buy ETF with ETH via swap
path = b'\x00' * 20 + b'\x01' + b'\x00' * 20 # WETH -> USDT path
purchaser.buy(
etf_token="0x123...",
etf_amount=1000000,
amount_in=50000000000000000000, # 50 ETH
recipient=user.address,
path=path,
min_amount=49000000, # 49 USDT minimum
value=50000000000000000000, # 50 ETH
sender=user
)
Purpose: Sells ETF tokens via Compass bridge.
Parameters:
_etf_token
: ETF token address to sell_etf_amount
: Amount of ETF tokens to sell_estimated_amount
: Estimated output amount_recipient
: Address to receive proceeds
Security Considerations:
- Validates ETF token address and amount
- Requires Paloma to be set
- Transfers tokens to contract, approves Compass, then bridges
- Uses Compass.send_token_to_paloma for cross-chain transfer
Usage Example:
# Sell ETF tokens
purchaser.sell(
etf_token="0x123...",
etf_amount=1000000, # 1 ETF token
estimated_amount=50000000, # 50 USDT estimated
recipient=user.address,
sender=user
)
Purpose: Validates Paloma cross-chain messages.
Security: Ensures sender is compass contract and Paloma identifier matches.
Purpose: Safely approves token spending.
Security: Uses default_return_value=True for compatibility.
Purpose: Safely transfers tokens.
Security: Only transfers if value > 0, uses default_return_value=True.
Purpose: Safely transfers tokens from another address.
Security: Only transfers if value > 0, uses default_return_value=True.
The contract emits the following events for transparency and off-chain tracking:
UpdateCompass
: Compass address updatesUpdateRefundWallet
: Refund wallet updatesUpdateFee
: Fee amount updatesUpdateFeeReceiver
: Fee receiver updatesSetPaloma
: Paloma identifier settingBuy
: ETF purchase transactionsSold
: ETF sale transactionsCreateSingleETF
: Single ETF creationRegisterSingleETF
: Single ETF registrationCreateCompositeETF
: Composite ETF creationRegisterCompositeETF
: Composite ETF registration
- Compass contract controls critical parameter updates
- Paloma validation required for sensitive operations
- Single-use Paloma setting prevents replay attacks
- All string parameters validated for non-empty values
- Address parameters validated for non-zero values
- Numeric parameters validated for positive values
- Expense ratios capped at 1,000,000 (100%)
@nonreentrant
decorator on all state-changing functions- External calls made after state updates
- Fee validation before operations
- Automatic refund of excess ETH
- Fee receiver validation
- Safe transfer patterns with default return values
- Proper approval and transfer sequences
- Decimal conversion handling
- Python 3.8+
- Ape Framework
- Foundry (for forking)
Currently, this repository does not contain test files. To add tests:
- Create a
tests/
directory - Add test files with
test_
prefix - Run tests with:
ape test
# tests/test_purchaser.py
import pytest
from ape import accounts, project
def test_constructor():
# Test contract deployment
pass
def test_buy_function():
# Test ETF purchase
pass
def test_sell_function():
# Test ETF sale
pass
def test_access_control():
# Test access restrictions
pass
The repository includes deployment scripts for multiple networks:
scripts/deploy_eth.py
- Ethereum mainnetscripts/deploy_arb.py
- Arbitrumscripts/deploy_base.py
- Basescripts/deploy_bsc.py
- BSCscripts/deploy_gnosis.py
- Gnosis Chainscripts/deploy_op.py
- Optimismscripts/deploy_polygon.py
- Polygon
To deploy:
# Load account
ape accounts load Deployer
# Deploy to specific network
ape run scripts/deploy_eth.py --network ethereum:mainnet
The contract supports deployment on multiple networks with fork testing capabilities:
- Ethereum (with Alchemy fork)
- Arbitrum (with Alchemy fork)
- BSC
- Gnosis Chain
- Base
- Optimism
- Polygon
Apache 2.0