5/5
## Understanding Vyper's `__default__` Fallback Function Smart contracts often need to receive Ether. While you can create specific functions like `fund()` or `deposit()` marked `@payable` to handle Ether sent alongside a function call, what happens if a user sends Ether directly to the contract's address without specifying a function? By default, a Vyper contract will reject such transfers. This lesson explores Vyper's special `__default__` function, which acts as a fallback mechanism to handle these direct transfers and calls to non-existent functions. **The Initial Scenario: A Payable `fund()` Function** Let's consider a simple Vyper contract, `buy_me_a_coffee.vy`, designed to accept funding. Initially, it has a `fund()` function: ```vyper # buy_me_a_coffee.vy (Initial Version Snippet) # Interface for Chainlink Price Feed (assumed) interface PriceFeed: def latestRoundData() -> (uint80, int256, uint256, uint256, uint80): view MINIMUM_USD: constant(uint256) = 5 * 10**18 # Example minimum $5 USD (assuming 18 decimals) price_feed: PriceFeed owner: address funders: DynArray[address, 100] # Track who funded funder_to_amount_funded: HashMap[address, uint256] # Track amounts # ... (constructor, _get_eth_to_usd_rate assumed) ... @external @payable def fund(): """Allows users to send $ to this contract Have a minimum $ amount to send """ # Check if minimum USD value is met (using an internal price feed function) usd_value_of_eth: uint256 = self._get_eth_to_usd_rate(msg.value) assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" # Track the funder and amount self.funders.append(msg.sender) self.funder_to_amount_funded[msg.sender] += msg.value ``` This contract works correctly when a user explicitly calls the `fund()` function and sends Ether along with the transaction. The function checks the value, tracks the funder, and updates their total contribution. **The Problem: Direct Ether Transfers** But what if someone obtains the contract address and uses a wallet like Metamask or a tool like Remix's "Low level interactions" to send Ether directly to the address *without* calling `fund()` (i.e., sending a transaction with a value but empty `CALLDATA`)? If we deploy the contract above and attempt such a direct Ether transfer (e.g., sending 1 ETH using Remix's low-level transact feature with empty `CALLDATA`), the transaction will **fail**. The Ethereum Virtual Machine (EVM) doesn't know how to handle this incoming Ether without a designated receiving mechanism. Remix might display an error similar to: `"In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function"`. While this error message uses Solidity terminology ("receive", "fallback"), it highlights the core issue: the contract isn't equipped to accept plain Ether transfers. **The Solution: Implementing the `__default__` Function** Vyper provides the `__default__` special function to address this. This function serves two primary purposes: 1. **Fallback for Non-Matching Function Calls:** If someone calls the contract with data (`CALLDATA`) that doesn't match any existing function signature, the `__default__` function is executed. 2. **Handling Direct Ether Transfers:** If the `__default__` function is marked `@payable`, it will also be executed when Ether is sent directly to the contract address with empty `CALLDATA`. To enable receiving direct Ether transfers, the `__default__` function must meet these criteria: * Must be named exactly `__default__`. * Must be annotated with `@external`. * Must be annotated with `@payable`. * Cannot accept any input arguments. Let's add a minimal `__default__` function to our contract: ```vyper # buy_me_a_coffee.vy (With Minimal __default__) # ... (previous code) ... @external @payable def fund(): # ... (same logic as before) ... @external @payable def __default__(): # This function does nothing for now pass ``` If we deploy this updated contract and attempt the direct Ether transfer again, the transaction will now **succeed**. The contract's Ether balance will increase. However, if we inspect the `funders` array or the `funder_to_amount_funded` map, we'll see they haven't been updated. This is because our `__default__` function simply contains `pass` – it accepts the Ether but doesn't execute our funding logic. **Refactoring for Consistent Logic** To ensure that *any* received funds (whether through `fund()` or a direct transfer) are tracked correctly, we need both entry points to execute the same core logic. The best practice is to refactor the logic into an internal function. 1. **Create an Internal Logic Function:** Move the core funding logic into a new function, marked `@internal` (conventionally prefixed with `_`). The `@payable` decorator is needed on the external entry points, not typically the internal function itself. ```vyper @internal def _fund(): # --- Same logic as original fund() --- usd_value_of_eth: uint256 = self._get_eth_to_usd_rate(msg.value) assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" self.funders.append(msg.sender) self.funder_to_amount_funded[msg.sender] += msg.value ``` 2. **Update the External `fund()` Function:** Modify the original `fund()` function to simply call the new internal logic function. ```vyper @external @payable def fund(): # Call the internal logic self._fund() ``` 3. **Update the `__default__` Function:** Modify the `__default__` function to also call the internal logic function. ```vyper @external @payable def __default__(): # Call the internal logic self._fund() ``` **Final Verification** Now, our contract is structured like this (relevant parts): ```vyper # buy_me_a_coffee.vy (Refactored Version Snippet) # ... (interfaces, state variables, constructor, _get_eth_to_usd_rate) ... @internal def _fund(): usd_value_of_eth: uint256 = self._get_eth_to_usd_rate(msg.value) assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" self.funders.append(msg.sender) self.funder_to_amount_funded[msg.sender] += msg.value @external @payable def fund(): self._fund() @external @payable def __default__(): self._fund() ``` With this refactored contract deployed: * Calling the `fund()` function with sufficient Ether works as before, executing `_fund()`. * Sending Ether directly to the contract address (e.g., via Metamask's "Send" or Remix's low-level transact with empty `CALLDATA`) now also works. The `@payable __default__()` function is triggered, which in turn calls `_fund()`. In both cases, the Ether is accepted, the contract's balance increases, and importantly, the `funders` array and `funder_to_amount_funded` map are correctly updated because the shared `_fund()` logic is executed regardless of how the Ether arrived. **Key Takeaways** * The `__default__` function is Vyper's mechanism for handling function calls that don't match any other signature and for receiving direct Ether transfers. * To receive Ether directly (without specific function data), `__default__` must be marked `@external` and `@payable` and accept no arguments. * If your contract needs to perform specific actions upon receiving Ether (like tracking funders), ensure the `__default__` function executes that logic, often by refactoring shared logic into an `@internal` function called by both `__default__` and other payable functions (like `fund()`). * Implementing `__default__` correctly improves user experience, allowing users to send funds directly to the contract address, and prevents funds from being accepted without triggering the intended contract logic.
__default__
Fallback FunctionSmart contracts often need to receive Ether. While you can create specific functions like fund()
or deposit()
marked @payable
to handle Ether sent alongside a function call, what happens if a user sends Ether directly to the contract's address without specifying a function? By default, a Vyper contract will reject such transfers. This lesson explores Vyper's special __default__
function, which acts as a fallback mechanism to handle these direct transfers and calls to non-existent functions.
The Initial Scenario: A Payable fund()
Function
Let's consider a simple Vyper contract, buy_me_a_coffee.vy
, designed to accept funding. Initially, it has a fund()
function:
This contract works correctly when a user explicitly calls the fund()
function and sends Ether along with the transaction. The function checks the value, tracks the funder, and updates their total contribution.
The Problem: Direct Ether Transfers
But what if someone obtains the contract address and uses a wallet like Metamask or a tool like Remix's "Low level interactions" to send Ether directly to the address without calling fund()
(i.e., sending a transaction with a value but empty CALLDATA
)?
If we deploy the contract above and attempt such a direct Ether transfer (e.g., sending 1 ETH using Remix's low-level transact feature with empty CALLDATA
), the transaction will fail. The Ethereum Virtual Machine (EVM) doesn't know how to handle this incoming Ether without a designated receiving mechanism. Remix might display an error similar to: "In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function"
. While this error message uses Solidity terminology ("receive", "fallback"), it highlights the core issue: the contract isn't equipped to accept plain Ether transfers.
The Solution: Implementing the __default__
Function
Vyper provides the __default__
special function to address this. This function serves two primary purposes:
Fallback for Non-Matching Function Calls: If someone calls the contract with data (CALLDATA
) that doesn't match any existing function signature, the __default__
function is executed.
Handling Direct Ether Transfers: If the __default__
function is marked @payable
, it will also be executed when Ether is sent directly to the contract address with empty CALLDATA
.
To enable receiving direct Ether transfers, the __default__
function must meet these criteria:
Must be named exactly __default__
.
Must be annotated with @external
.
Must be annotated with @payable
.
Cannot accept any input arguments.
Let's add a minimal __default__
function to our contract:
If we deploy this updated contract and attempt the direct Ether transfer again, the transaction will now succeed. The contract's Ether balance will increase. However, if we inspect the funders
array or the funder_to_amount_funded
map, we'll see they haven't been updated. This is because our __default__
function simply contains pass
– it accepts the Ether but doesn't execute our funding logic.
Refactoring for Consistent Logic
To ensure that any received funds (whether through fund()
or a direct transfer) are tracked correctly, we need both entry points to execute the same core logic. The best practice is to refactor the logic into an internal function.
Create an Internal Logic Function: Move the core funding logic into a new function, marked @internal
(conventionally prefixed with _
). The @payable
decorator is needed on the external entry points, not typically the internal function itself.
Update the External fund()
Function: Modify the original fund()
function to simply call the new internal logic function.
Update the __default__
Function: Modify the __default__
function to also call the internal logic function.
Final Verification
Now, our contract is structured like this (relevant parts):
With this refactored contract deployed:
Calling the fund()
function with sufficient Ether works as before, executing _fund()
.
Sending Ether directly to the contract address (e.g., via Metamask's "Send" or Remix's low-level transact with empty CALLDATA
) now also works. The @payable __default__()
function is triggered, which in turn calls _fund()
.
In both cases, the Ether is accepted, the contract's balance increases, and importantly, the funders
array and funder_to_amount_funded
map are correctly updated because the shared _fund()
logic is executed regardless of how the Ether arrived.
Key Takeaways
The __default__
function is Vyper's mechanism for handling function calls that don't match any other signature and for receiving direct Ether transfers.
To receive Ether directly (without specific function data), __default__
must be marked @external
and @payable
and accept no arguments.
If your contract needs to perform specific actions upon receiving Ether (like tracking funders), ensure the __default__
function executes that logic, often by refactoring shared logic into an @internal
function called by both __default__
and other payable functions (like fund()
).
Implementing __default__
correctly improves user experience, allowing users to send funds directly to the contract address, and prevents funds from being accepted without triggering the intended contract logic.
A practical guide to Understanding Vyper's `__default__` Fallback Function - Learn how this special function enables contracts to receive direct Ether transfers and handle unmatched function calls, ensuring robust fund acceptance through proper implementation and refactoring.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025
Duration: 2h 08min
Duration: 2h 32min
Duration: 24min
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025