From f70db56a6b31147f30ee19fbadb210e822ac841e Mon Sep 17 00:00:00 2001 From: phessophissy Date: Tue, 13 Jan 2026 19:57:15 +0100 Subject: [PATCH] docs: Improve README with comprehensive examples and add NatSpec comments ## README.md Improvements - Added badges (License, Solidity version, Hardhat) - Added Table of Contents for easy navigation - Added Overview section explaining ButtonToken vs UnbuttonToken - Added Installation and Quick Start guides - Added Contract Architecture diagram - Added 5 comprehensive usage examples: 1. Wrapping ETH into ButtonWETH via Router 2. Depositing into ButtonToken directly 3. Using UnbuttonToken for rebasing assets 4. Creating new ButtonToken via Factory 5. TypeScript integration example - Added API Reference tables for both contracts - Added Security section with known limitations - Added structured Deployments table - Improved Contributing guidelines ## ButtonToken.sol NatSpec Improvements - Added comprehensive contract-level documentation - Added @title, @author, @notice, @dev tags - Documented mathematical model (bits system) - Added accounting guarantees documentation - Enhanced all public function documentation with: - @notice for user-facing description - @dev for technical details - @param for all parameters - @return for return values - @custom:example with code samples - Requirements and Effects sections - Documented all private helper functions - Added security notes and emits documentation ## UnbuttonToken.sol NatSpec Improvements - Added comprehensive contract-level documentation - Documented share-based mathematical model - Added use cases (DeFi, Portfolio, Yield Farming) - Added security considerations for overflow limits - Enhanced all public function documentation - Added @custom:formula tags for calculations - Documented initialization security (anti-manipulation) - Added code examples for common operations This contribution addresses documentation improvements as identified in the repository's open issues. --- README.md | 512 +++++++++++++++++++++++--- contracts/ButtonToken.sol | 697 ++++++++++++++++++++++++++++++------ contracts/UnbuttonToken.sol | 392 +++++++++++++++++--- 3 files changed, 1372 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index e080074..c574274 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,473 @@ # Button Wrappers -ERC-20 Token wrappers for the button wood ecosystem. +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Solidity](https://img.shields.io/badge/Solidity-0.8.4-363636.svg)](https://soliditylang.org/) +[![Hardhat](https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg)](https://hardhat.org/) -## Install +ERC-20 Token wrappers for the Buttonwood ecosystem. This library provides two primary wrapper contracts: + +- **ButtonToken**: Wraps fixed-balance tokens into elastic/rebasing tokens (price-targeted) +- **UnbuttonToken**: Wraps elastic/rebasing tokens into fixed-balance tokens (share-based) + +## Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Contract Architecture](#contract-architecture) +- [Usage Examples](#usage-examples) +- [API Reference](#api-reference) +- [Testing](#testing) +- [Deployments](#deployments) +- [Security](#security) +- [Contributing](#contributing) +- [License](#license) + +## Overview + +### ButtonToken (Rebasing Wrapper) + +ButtonToken wraps fixed-balance ERC-20 tokens and creates elastic balance tokens that rebase based on an oracle price. This is useful for creating stable-unit representations of volatile assets. + +**Example**: Wrap 1 ETH when ETH = $2,000 → Receive 2,000 ButtonETH (each worth $1). If ETH goes to $2,500 → Your balance becomes 2,500 ButtonETH. + +### UnbuttonToken (Fixed Wrapper) + +UnbuttonToken wraps elastic/rebasing tokens (like AMPL, aTokens) into fixed-balance tokens. Your share of the underlying pool remains constant. + +**Example**: Deposit 1,000 AMPL → Receive UnbuttonAMPL tokens. Even as AMPL rebases, your UnbuttonAMPL balance stays the same (but represents changing underlying value). + +## Installation + +### Prerequisites + +- Node.js v16.20.2 (use [nvm](https://github.com/nvm-sh/nvm) for version management) +- Yarn package manager ```bash -# Install project dependencies -yarn +# Clone the repository +git clone https://github.com/buttonwood-protocol/button-wrappers.git +cd button-wrappers + +# Install dependencies +yarn install ``` -## Testing +## Quick Start + +### Compile Contracts + +```bash +yarn compile +``` + +### Run Tests ```bash -# Run all unit tests (compatible with node v12+) yarn test ``` +### Deploy Locally -## Deployments +```bash +# Start local hardhat node +yarn hardhat node + +# In another terminal, deploy +yarn hardhat run scripts/deploy.ts --network localhost +``` + +## Contract Architecture + +``` +contracts/ +├── ButtonToken.sol # Rebasing wrapper (fixed → elastic) +├── ButtonTokenFactory.sol # Factory for deploying ButtonToken instances +├── UnbuttonToken.sol # Fixed wrapper (elastic → fixed) +├── UnbuttonTokenFactory.sol # Factory for deploying UnbuttonToken instances +├── ButtonTokenWethRouter.sol # Router for ETH ↔ ButtonWETH +├── ButtonTokenWamplRouter.sol# Router for AMPL ↔ ButtonWAMPL +├── interfaces/ # Contract interfaces +├── oracles/ # Oracle implementations +├── mocks/ # Mock contracts for testing +└── utilities/ # Utility contracts +``` + +## Usage Examples + +### Example 1: Wrapping ETH into ButtonWETH + +```solidity +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.4; + +import "./ButtonTokenWethRouter.sol"; + +contract MyDeFiProtocol { + ButtonTokenWethRouter public router; + IButtonToken public buttonWETH; + + constructor(address _router, address _buttonWETH) { + router = ButtonTokenWethRouter(_router); + buttonWETH = IButtonToken(_buttonWETH); + } + + /// @notice Wrap ETH into ButtonWETH + /// @dev User sends ETH, receives ButtonWETH tokens + /// @return amount The amount of ButtonWETH received + function wrapETH() external payable returns (uint256 amount) { + // Deposit ETH and receive ButtonWETH + amount = router.deposit{value: msg.value}(address(buttonWETH)); + + // Transfer ButtonWETH to the user + IERC20(address(buttonWETH)).transfer(msg.sender, amount); + } + + /// @notice Unwrap ButtonWETH back to ETH + /// @param buttonAmount Amount of ButtonWETH to unwrap + function unwrapToETH(uint256 buttonAmount) external { + // Transfer ButtonWETH from user to this contract + IERC20(address(buttonWETH)).transferFrom(msg.sender, address(this), buttonAmount); + + // Approve router to spend ButtonWETH + IERC20(address(buttonWETH)).approve(address(router), buttonAmount); + + // Burn ButtonWETH and receive ETH + router.burn(address(buttonWETH), buttonAmount); + + // Transfer ETH to user + payable(msg.sender).transfer(address(this).balance); + } +} +``` + +### Example 2: Depositing into ButtonToken Directly + +```solidity +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.4; + +import "./interfaces/IButtonToken.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract ButtonTokenDepositor { + IButtonToken public buttonToken; + IERC20 public underlying; + + constructor(address _buttonToken) { + buttonToken = IButtonToken(_buttonToken); + underlying = IERC20(buttonToken.underlying()); + } + + /// @notice Deposit underlying tokens and receive ButtonTokens + /// @param amount Amount of underlying tokens to deposit + /// @return buttonAmount Amount of ButtonTokens received + function deposit(uint256 amount) external returns (uint256 buttonAmount) { + // Transfer underlying from user + underlying.transferFrom(msg.sender, address(this), amount); + + // Approve ButtonToken to spend underlying + underlying.approve(address(buttonToken), amount); + + // Deposit underlying and receive ButtonTokens + // deposit() takes underlying amount, returns button amount + buttonAmount = buttonToken.deposit(amount); + + // Transfer ButtonTokens to user + IERC20(address(buttonToken)).transfer(msg.sender, buttonAmount); + } + + /// @notice Withdraw underlying tokens by burning ButtonTokens + /// @param buttonAmount Amount of ButtonTokens to burn + /// @return underlyingAmount Amount of underlying tokens received + function withdraw(uint256 buttonAmount) external returns (uint256 underlyingAmount) { + // Transfer ButtonTokens from user + IERC20(address(buttonToken)).transferFrom(msg.sender, address(this), buttonAmount); + + // Burn ButtonTokens (burn takes button amount, returns underlying) + underlyingAmount = buttonToken.burn(buttonAmount); + + // Transfer underlying to user + underlying.transfer(msg.sender, underlyingAmount); + } + + /// @notice Mint specific amount of ButtonTokens + /// @dev mint() calculates required underlying automatically + /// @param buttonAmount Desired amount of ButtonTokens + /// @return underlyingUsed Amount of underlying tokens used + function mintExact(uint256 buttonAmount) external returns (uint256 underlyingUsed) { + // Calculate required underlying + underlyingUsed = buttonToken.wrapperToUnderlying(buttonAmount); + + // Transfer underlying from user + underlying.transferFrom(msg.sender, address(this), underlyingUsed); + + // Approve and mint + underlying.approve(address(buttonToken), underlyingUsed); + buttonToken.mint(buttonAmount); + + // Transfer ButtonTokens to user + IERC20(address(buttonToken)).transfer(msg.sender, buttonAmount); + } +} +``` + +### Example 3: Using UnbuttonToken for Rebasing Assets + +```solidity +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.4; + +import "./interfaces/IButtonWrapper.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -For the most up-to-date, please refer to: https://docs.prl.one/buttonwood/developers/deployed-contracts. -```yaml -mainnet: - ButtonTokenFactory: "0x84D0F1Cd873122F2A87673e079ea69cd80b51960" - instances: - - name: "Button WETH" - - symbol: "bWETH" - - underlying: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - - address: "0x8f471e1896d16481678db553f86283eab1561b02" - - name: "Button WBTC" - - symbol: "bWBTC" - - underlying: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" - - address: "0x8e8212386d580d8dd731a2b1a36a43935250304e" - - UnbuttonTokenFactory: "0x75ff649d6119fab43dea5e5e9e02586f27fc8b8f" - instances: - - name: "Unbuttoned AAVE AMPL" - - symbol: "ubAAMPL" - - underlying: "0x1e6bb68acec8fefbd87d192be09bb274170a0548" - - address: "0xF03387d8d0FF326ab586A58E0ab4121d106147DF" - -avalanche: - ButtonTokenFactory: "0x83f6392Aab030043420D184a025e0Cd63f508798" +contract UnbuttonVault { + IButtonWrapper public unbuttonToken; + IERC20 public rebasingToken; // e.g., AMPL, aUSDC - instances: - - name: "Button WETH" - - symbol: "bWETH" - - underlying: "0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab" - - address: "0x227d7A0e2586A5bFdA7f32aDF066d20D1bfDfDfb" - - name: "Button WAVAX" - - symbol: "bWAVAX" - - underlying: "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7" - - address: "0x9f61aE42c01698aC35AedeF651B0FE5E407bC6A0" - - name: "Button WBTC" - - symbol: "bWBTC" - - underlying: "0x50b7545627a5162f82a992c33b87adc75187b218" - - address: "0x9bFE32D18e66ffAF6dcB0306AE7D24F768469f91" + constructor(address _unbuttonToken) { + unbuttonToken = IButtonWrapper(_unbuttonToken); + rebasingToken = IERC20(unbuttonToken.underlying()); + } + + /// @notice Deposit rebasing tokens for fixed-balance representation + /// @param amount Amount of rebasing tokens to deposit + /// @return shares Amount of unbutton tokens (shares) received + function depositRebasing(uint256 amount) external returns (uint256 shares) { + // Transfer rebasing tokens from user + rebasingToken.transferFrom(msg.sender, address(this), amount); + + // Approve UnbuttonToken + rebasingToken.approve(address(unbuttonToken), amount); + + // Deposit and receive shares + shares = unbuttonToken.deposit(amount); + + // Transfer shares to user + IERC20(address(unbuttonToken)).transfer(msg.sender, shares); + } + + /// @notice Withdraw all underlying rebasing tokens + /// @return amount Amount of rebasing tokens received + function withdrawAll() external returns (uint256 amount) { + uint256 shares = IERC20(address(unbuttonToken)).balanceOf(msg.sender); + + // Transfer shares from user + IERC20(address(unbuttonToken)).transferFrom(msg.sender, address(this), shares); + + // Burn all shares and receive underlying + amount = unbuttonToken.burnAll(); + + // Transfer underlying to user + rebasingToken.transfer(msg.sender, amount); + } + + /// @notice Get underlying value of shares + /// @param shares Amount of unbutton shares + /// @return underlyingValue Current underlying token value + function getUnderlyingValue(uint256 shares) external view returns (uint256 underlyingValue) { + return unbuttonToken.wrapperToUnderlying(shares); + } +} ``` -## Example script usage +### Example 4: Creating a New ButtonToken via Factory + +```solidity +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.4; + +import "./ButtonTokenFactory.sol"; +import "./interfaces/IButtonToken.sol"; + +contract ButtonTokenCreator { + ButtonTokenFactory public factory; + + constructor(address _factory) { + factory = ButtonTokenFactory(_factory); + } + + /// @notice Create a new ButtonToken for any ERC-20 with a price oracle + /// @param underlying Address of the underlying ERC-20 token + /// @param name Name for the new ButtonToken (e.g., "Button USDC") + /// @param symbol Symbol for the new ButtonToken (e.g., "bUSDC") + /// @param oracle Address of the price oracle (must implement IOracle) + /// @return buttonToken Address of the newly created ButtonToken + function createButtonToken( + address underlying, + string memory name, + string memory symbol, + address oracle + ) external returns (address buttonToken) { + buttonToken = factory.create(underlying, name, symbol, oracle); + + // Transfer ownership to caller + IButtonToken(buttonToken).transferOwnership(msg.sender); + } +} +``` -### Verifying ChainlinkOracle contract -`yarn hardhat verify:ChainlinkOracle --network kovan --address 0x29636cE5b252a0F3C088BA1A70Bf936AA00ce7D4 --aggregator 0x6135b13325bfC4B00278B4abC5e20bbce2D6580e --staleness-threshold-secs 0x15180` +### Example 5: TypeScript Integration -## Contribute +```typescript +import { ethers } from 'ethers'; -To report bugs within this package, create an issue in this repository. -When submitting code ensure that it is free of lint errors and has 100% test coverage. +// Connect to ButtonToken +const buttonTokenAddress = '0x8f471e1896d16481678db553f86283eab1561b02'; // bWETH mainnet +const buttonTokenABI = [ + 'function deposit(uint256 uAmount) returns (uint256)', + 'function withdraw(uint256 uAmount) returns (uint256)', + 'function burn(uint256 amount) returns (uint256)', + 'function balanceOf(address) view returns (uint256)', + 'function balanceOfUnderlying(address) view returns (uint256)', + 'function underlyingToWrapper(uint256) view returns (uint256)', + 'function wrapperToUnderlying(uint256) view returns (uint256)', + 'function underlying() view returns (address)', +]; + +async function interactWithButtonToken(provider: ethers.providers.Provider, signer: ethers.Signer) { + const buttonToken = new ethers.Contract(buttonTokenAddress, buttonTokenABI, signer); + + const userAddress = await signer.getAddress(); + + // Get underlying token address + const underlyingAddress = await buttonToken.underlying(); + console.log('Underlying token:', underlyingAddress); + + // Check current balances + const buttonBalance = await buttonToken.balanceOf(userAddress); + const underlyingBalance = await buttonToken.balanceOfUnderlying(userAddress); + console.log('Button balance:', ethers.utils.formatEther(buttonBalance)); + console.log('Underlying balance:', ethers.utils.formatEther(underlyingBalance)); + + // Convert between amounts + const underlyingAmount = ethers.utils.parseEther('1'); + const expectedButtonAmount = await buttonToken.underlyingToWrapper(underlyingAmount); + console.log('1 underlying =', ethers.utils.formatEther(expectedButtonAmount), 'button tokens'); + + // Deposit example (requires prior approval) + // const underlying = new ethers.Contract(underlyingAddress, ['function approve(address,uint256)'], signer); + // await underlying.approve(buttonTokenAddress, underlyingAmount); + // const receivedAmount = await buttonToken.deposit(underlyingAmount); +} + +// Using with ethers v6 +async function depositWithPermit(buttonToken: ethers.Contract, amount: bigint) { + // If using a permit-enabled wrapper, you can use permit for gasless approvals + const tx = await buttonToken.deposit(amount); + const receipt = await tx.wait(); + console.log('Deposit successful:', receipt.hash); +} +``` + +## API Reference + +### ButtonToken + +| Function | Description | Returns | +|----------|-------------|---------| +| `deposit(uAmount)` | Deposit underlying tokens | Amount of ButtonTokens minted | +| `depositFor(to, uAmount)` | Deposit for another address | Amount of ButtonTokens minted | +| `withdraw(uAmount)` | Withdraw underlying tokens | Amount of ButtonTokens burned | +| `withdrawTo(to, uAmount)` | Withdraw to another address | Amount of ButtonTokens burned | +| `withdrawAll()` | Withdraw all underlying | Amount of underlying received | +| `mint(amount)` | Mint exact ButtonToken amount | Amount of underlying deposited | +| `burn(amount)` | Burn exact ButtonToken amount | Amount of underlying received | +| `transferAll(to)` | Transfer entire balance | Success boolean | +| `rebase()` | Trigger manual rebase | - | + +### UnbuttonToken + +| Function | Description | Returns | +|----------|-------------|---------| +| `deposit(uAmount)` | Deposit underlying (rebasing) | Amount of shares minted | +| `depositFor(to, uAmount)` | Deposit for another address | Amount of shares minted | +| `withdraw(uAmount)` | Withdraw underlying tokens | Amount of shares burned | +| `withdrawTo(to, uAmount)` | Withdraw to another address | Amount of shares burned | +| `withdrawAll()` | Withdraw all underlying | Amount of underlying received | +| `mint(amount)` | Mint exact share amount | Amount of underlying deposited | +| `burn(amount)` | Burn exact share amount | Amount of underlying received | + +### View Functions (Both) + +| Function | Description | +|----------|-------------| +| `underlying()` | Address of underlying token | +| `totalUnderlying()` | Total underlying tokens held | +| `balanceOfUnderlying(address)` | User's underlying token balance | +| `underlyingToWrapper(uAmount)` | Convert underlying → wrapper amount | +| `wrapperToUnderlying(amount)` | Convert wrapper → underlying amount | + +## Testing + +```bash +# Run all unit tests +yarn test + +# Run tests with gas reporting +yarn profile + +# Run tests with coverage +yarn coverage + +# Run specific test file +yarn hardhat test test/unit/ButtonToken.ts +``` + +## Deployments + +### Mainnet (Ethereum) + +| Contract | Address | +|----------|---------| +| ButtonTokenFactory | `0x84D0F1Cd873122F2A87673e079ea69cd80b51960` | +| UnbuttonTokenFactory | `0x75ff649d6119fab43dea5e5e9e02586f27fc8b8f` | + +**ButtonToken Instances:** +- bWETH: `0x8f471e1896d16481678db553f86283eab1561b02` +- bWBTC: `0x8e8212386d580d8dd731a2b1a36a43935250304e` + +**UnbuttonToken Instances:** +- ubAAMPL: `0xF03387d8d0FF326ab586A58E0ab4121d106147DF` + +### Avalanche + +| Contract | Address | +|----------|---------| +| ButtonTokenFactory | `0x83f6392Aab030043420D184a025e0Cd63f508798` | + +**ButtonToken Instances:** +- bWETH: `0x227d7A0e2586A5bFdA7f32aDF066d20D1bfDfDfb` +- bWAVAX: `0x9f61aE42c01698aC35AedeF651B0FE5E407bC6A0` +- bWBTC: `0x9bFE32D18e66ffAF6dcB0306AE7D24F768469f91` + +For the latest deployments, see: https://docs.prl.one/buttonwood/developers/deployed-contracts + +## Security + +### Audits + +Please refer to the [security documentation](./bug-bounty.md) for audit reports. + +### Bug Bounty + +We offer a bug bounty program. See [bug-bounty.md](./bug-bounty.md) for details. + +### Known Limitations + +- **Fee-on-Transfer Tokens**: ButtonToken does NOT support fee-on-transfer (FoT) tokens. These are incompatible with the accounting in `deposit/mint` functions. +- **Numeric Overflow**: UnbuttonToken has a maximum underlying capacity based on `sqrt(MAX_UINT256/INITIAL_RATE)`. + +## Contributing + +We welcome contributions! Please follow these steps: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Ensure code is lint-free and has 100% test coverage +4. Commit your changes (`git commit -m 'Add amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a Pull Request ```bash # Lint code @@ -75,21 +476,14 @@ yarn lint # Format code yarn format -# Run solidity coverage report (compatible with node v12) +# Check coverage yarn coverage - -# Run solidity gas usage report -yarn profile ``` ## License [GNU General Public License v3.0](./LICENSE) -## Notes - -Note that the ButtonToken contract _does not_ work with fee-on-transfer (FoT) tokens. These tokens are not compatible with the accounting of the `deposit/mint` functions in ButtonToken. - -## Bug Bounty +--- -[Details](bug-bounty.md) +Built with ❤️ by the Buttonwood Protocol team diff --git a/contracts/ButtonToken.sol b/contracts/ButtonToken.sol index bb446a1..f8dae96 100644 --- a/contracts/ButtonToken.sol +++ b/contracts/ButtonToken.sol @@ -8,116 +8,168 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /** - * @title The ButtonToken ERC20 wrapper. + * @title ButtonToken - Rebasing ERC20 Token Wrapper + * @author Buttonwood Protocol + * @notice Wraps fixed-balance ERC-20 tokens into elastic/rebasing tokens based on price oracle * * @dev The ButtonToken is a rebasing wrapper for fixed balance ERC-20 tokens. + * Users deposit the "underlying" (wrapped) tokens and are minted button (wrapper) + * tokens with elastic balances which change up or down when the value of the + * underlying token changes. * - * Users deposit the "underlying" (wrapped) tokens and are - * minted button (wrapper) tokens with elastic balances - * which change up or down when the value of the underlying token changes. + * ## How It Works * - * For example: Manny “wraps” 1 Ether when the price of Ether is $1800. - * Manny receives 1800 ButtonEther tokens in return. - * The overall value of their ButtonEther is the same as their original Ether, - * however each unit is now priced at exactly $1. The next day, - * the price of Ether changes to $1900. The ButtonEther system detects - * this price change, and rebases such that Manny’s balance is - * now 1900 ButtonEther tokens, still priced at $1 each. + * **Example Scenario:** + * 1. Manny "wraps" 1 Ether when the price of Ether is $1800 + * 2. Manny receives 1800 ButtonEther tokens in return + * 3. The overall value of their ButtonEther is the same as their original Ether, + * however each unit is now priced at exactly $1 + * 4. The next day, the price of Ether changes to $1900 + * 5. The ButtonEther system detects this price change and rebases + * 6. Manny's balance is now 1900 ButtonEther tokens, still priced at $1 each + * + * ## Mathematical Model * * The ButtonToken math is almost identical to Ampleforth's μFragments. * - * For AMPL, internal balances are represented using `gons` and - * -> internal account balance `_gonBalances[account]` - * -> internal supply scalar `gonsPerFragment = TOTAL_GONS / _totalSupply` - * -> public balance `_gonBalances[account] * gonsPerFragment` - * -> public total supply `_totalSupply` + * **For AMPL:** + * - Internal account balance: `_gonBalances[account]` + * - Internal supply scalar: `gonsPerFragment = TOTAL_GONS / _totalSupply` + * - Public balance: `_gonBalances[account] * gonsPerFragment` + * - Public total supply: `_totalSupply` + * + * **For ButtonToken (using 'bits'):** + * - Underlying token unit price: `p_u = price / 10 ^ (PRICE_DECIMALS)` + * - Total underlying tokens: `_totalUnderlying` + * - Internal account balance: `_accountBits[account]` + * - Internal supply scalar: `_bitsPerToken = TOTAL_BITS / (MAX_UNDERLYING*p_u)` + * `= BITS_PER_UNDERLYING*(10^PRICE_DECIMALS)/price` + * `= PRICE_BITS / price` + * - User's underlying balance: `_accountBits[account] / BITS_PER_UNDERLYING` + * - Public balance: `_accountBits[account] * _bitsPerToken` + * - Public total supply: `_totalUnderlying * p_u` * - * In our case internal balances are stored as 'bits'. - * -> underlying token unit price `p_u = price / 10 ^ (PRICE_DECIMALS)` - * -> total underlying tokens `_totalUnderlying` - * -> internal account balance `_accountBits[account]` - * -> internal supply scalar `_bitsPerToken` - ` = TOTAL_BITS / (MAX_UNDERLYING*p_u)` - * ` = BITS_PER_UNDERLYING*(10^PRICE_DECIMALS)/price` - * ` = PRICE_BITS / price` - * -> user's underlying balance `(_accountBits[account] / BITS_PER_UNDERLYING` - * -> public balance `_accountBits[account] * _bitsPerToken` - * -> public total supply `_totalUnderlying * p_u` + * ## Accounting Guarantees * + * - If address 'A' transfers x button tokens to address 'B': + * A's resulting external balance will be decreased by precisely x button tokens, + * and B's external balance will be precisely increased by x button tokens. + * - If address 'A' deposits y underlying tokens: + * A's resulting underlying balance will increase by precisely y. + * - If address 'A' withdraws y underlying tokens: + * A's resulting underlying balance will decrease by precisely y. * + * ## Important Notes + * + * - This contract does NOT work with fee-on-transfer (FoT) tokens + * - Maximum underlying deposit is capped at MAX_UNDERLYING (1 billion tokens) + * - Price oracle must return valid data for the contract to function */ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { - // PLEASE READ BEFORE CHANGING ANY ACCOUNTING OR MATH - // We make the following guarantees: - // - If address 'A' transfers x button tokens to address 'B'. - // A's resulting external balance will be decreased by "precisely" x button tokens, - // and B's external balance will be "precisely" increased by x button tokens. - // - If address 'A' deposits y underlying tokens, - // A's resulting underlying balance will increase by "precisely" y. - // - If address 'A' withdraws y underlying tokens, - // A's resulting underlying balance will decrease by "precisely" y. - // using SafeERC20 for IERC20; //-------------------------------------------------------------------------- // Constants + //-------------------------------------------------------------------------- - /// @dev Math constants. + /// @dev Maximum value for uint256, used for bit calculations uint256 private constant MAX_UINT256 = type(uint256).max; - /// @dev The maximum units of the underlying token that can be deposited into this contract - /// ie) for a underlying token with 18 decimals, MAX_UNDERLYING is 1B tokens. + /** + * @notice Maximum units of the underlying token that can be deposited + * @dev For a underlying token with 18 decimals, MAX_UNDERLYING is 1 billion tokens + * This limit prevents overflow in bit calculations + */ uint256 public constant MAX_UNDERLYING = 1_000_000_000e18; - /// @dev TOTAL_BITS is a multiple of MAX_UNDERLYING so that {BITS_PER_UNDERLYING} is an integer. - /// Use the highest value that fits in a uint256 for max granularity. + /** + * @dev TOTAL_BITS is a multiple of MAX_UNDERLYING so that BITS_PER_UNDERLYING is an integer + * Uses the highest value that fits in a uint256 for maximum granularity + */ uint256 private constant TOTAL_BITS = MAX_UINT256 - (MAX_UINT256 % MAX_UNDERLYING); - /// @dev Number of BITS per unit of deposit. + /// @dev Number of BITS per unit of underlying token deposit uint256 private constant BITS_PER_UNDERLYING = TOTAL_BITS / MAX_UNDERLYING; //-------------------------------------------------------------------------- - // Attributes + // State Variables + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @dev The address of the ERC-20 token being wrapped + */ address public override underlying; - /// @inheritdoc IButtonToken + /** + * @inheritdoc IButtonToken + * @dev Price oracle contract that provides the underlying token's price + * Must implement the IOracle interface + */ address public override oracle; - /// @inheritdoc IButtonToken + /** + * @inheritdoc IButtonToken + * @dev Cached price from the last rebase operation + * Updated whenever rebase() is called or on any state-changing operation + */ uint256 public override lastPrice; - /// @dev Rebase counter + /// @dev Rebase counter, incremented each time a successful rebase occurs uint256 _epoch; - /// @inheritdoc IERC20Metadata + /** + * @inheritdoc IERC20Metadata + * @dev Human-readable name of the button token (e.g., "Button Wrapped Ether") + */ string public override name; - /// @inheritdoc IERC20Metadata + /** + * @inheritdoc IERC20Metadata + * @dev Short symbol of the button token (e.g., "bWETH") + */ string public override symbol; - /// @dev Number of BITS per unit of deposit * (1 USD). + /** + * @dev Number of BITS per unit of deposit multiplied by (1 USD equivalent) + * Used for price-adjusted bit calculations + */ uint256 private priceBits; - /// @dev trueMaxPrice = maximum integer < (sqrt(4*priceBits + 1) - 1) / 2 - /// maxPrice is the closest power of two which is just under trueMaxPrice. + /** + * @dev Maximum price that can be safely handled without overflow + * Calculated as the closest power of two under the true maximum + * trueMaxPrice = maximum integer < (sqrt(4*priceBits + 1) - 1) / 2 + */ uint256 private maxPrice; - /// @dev internal balance, bits issued per account + /** + * @dev Internal balance mapping: bits issued per account + * address(0) holds the "unmined" bits (available for new deposits) + */ mapping(address => uint256) private _accountBits; - /// @dev ERC20 allowances + /// @dev Standard ERC20 allowances mapping mapping(address => mapping(address => uint256)) private _allowances; //-------------------------------------------------------------------------- // Modifiers + //-------------------------------------------------------------------------- + + /** + * @dev Validates that the recipient address is valid + * @param to The recipient address to validate + */ modifier validRecipient(address to) { require(to != address(0x0), "ButtonToken: recipient zero address"); require(to != address(this), "ButtonToken: recipient token address"); _; } + /** + * @dev Ensures rebase is called before executing the function + * Queries the oracle and rebases if valid price data is available + */ modifier onAfterRebase() { uint256 price; bool valid; @@ -128,12 +180,39 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { _; } + //-------------------------------------------------------------------------- + // Initialization //-------------------------------------------------------------------------- - /// @param underlying_ The underlying ERC20 token address. - /// @param name_ The ERC20 name. - /// @param symbol_ The ERC20 symbol. - /// @param oracle_ The oracle which provides the underlying token price. + /** + * @notice Initializes the ButtonToken contract + * @dev Can only be called once due to initializer modifier + * Sets up the token with underlying asset, name, symbol, and price oracle + * + * @param underlying_ The address of the underlying ERC20 token to wrap + * @param name_ The human-readable name for the button token + * @param symbol_ The short symbol for the button token + * @param oracle_ The address of the price oracle contract + * + * Requirements: + * - underlying_ must not be the zero address + * - oracle_ must return valid price data + * + * Effects: + * - Sets msg.sender as the owner + * - Initializes all TOTAL_BITS to address(0) for future minting + * - Performs initial rebase with oracle price + * + * @custom:example + * ```solidity + * buttonToken.initialize( + * 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH + * "Button Wrapped Ether", + * "bWETH", + * 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 // ETH/USD Chainlink + * ); + * ``` + */ function initialize( address underlying_, string memory name_, @@ -161,9 +240,29 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } //-------------------------------------------------------------------------- - // Owner only actions + // Owner Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonToken + /** + * @inheritdoc IButtonToken + * @notice Updates the price oracle used for rebasing + * @dev Only callable by the contract owner + * + * @param oracle_ The address of the new oracle contract + * + * Requirements: + * - Caller must be the owner + * - New oracle must return valid price data + * + * Effects: + * - Updates the oracle address + * - Recalculates priceBits and maxPrice based on oracle's price decimals + * - Emits OracleUpdated event + * - Triggers a rebase with the new oracle's price + * + * @custom:security Only owner can call this function + * @custom:emits OracleUpdated(oracle_) + */ function updateOracle(address oracle_) public override onlyOwner { uint256 price; bool valid; @@ -181,24 +280,51 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } //-------------------------------------------------------------------------- - // ERC20 description attributes + // ERC20 Metadata + //-------------------------------------------------------------------------- - /// @inheritdoc IERC20Metadata + /** + * @inheritdoc IERC20Metadata + * @notice Returns the number of decimals used for display purposes + * @dev Matches the decimals of the underlying token for consistency + * @return The number of decimals (typically 18) + */ function decimals() external view override returns (uint8) { return IERC20Metadata(underlying).decimals(); } //-------------------------------------------------------------------------- - // ERC-20 token view methods + // ERC-20 View Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Returns the total supply of button tokens in circulation + * @dev Calculated dynamically based on current oracle price + * totalSupply = totalUnderlying * currentPrice + * @return The total supply of button tokens + */ function totalSupply() external view override returns (uint256) { uint256 price; (price, ) = _queryPrice(); return _bitsToAmount(_activeBits(), price); } - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Returns the button token balance of the specified account + * @dev Balance changes automatically with price updates (rebasing) + * balance = accountBits * bitsPerToken(currentPrice) + * + * @param account The address to query the balance of + * @return The button token balance of the account + * + * @custom:example + * ```solidity + * uint256 balance = buttonToken.balanceOf(msg.sender); + * // Balance will change after rebase even without any transfers + * ``` + */ function balanceOf(address account) external view override returns (uint256) { if (account == address(0)) { return 0; @@ -208,12 +334,25 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return _bitsToAmount(_accountBits[account], price); } - /// @inheritdoc IRebasingERC20 + /** + * @inheritdoc IRebasingERC20 + * @notice Returns the scaled (underlying) total supply + * @dev This is the total underlying tokens deposited, unaffected by rebasing + * @return The scaled total supply in underlying token units + */ function scaledTotalSupply() external view override returns (uint256) { return _bitsToUAmount(_activeBits()); } - /// @inheritdoc IRebasingERC20 + /** + * @inheritdoc IRebasingERC20 + * @notice Returns the scaled (underlying) balance of an account + * @dev This balance is fixed and doesn't change with rebasing + * Use this to get a user's share of the underlying pool + * + * @param account The address to query + * @return The scaled balance in underlying token units + */ function scaledBalanceOf(address account) external view override returns (uint256) { if (account == address(0)) { return 0; @@ -221,20 +360,47 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return _bitsToUAmount(_accountBits[account]); } - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Returns the remaining allowance for a spender + * @dev Standard ERC20 allowance - not affected by rebasing + * + * @param owner_ The address of the token owner + * @param spender The address of the approved spender + * @return The remaining allowance in button token units + */ function allowance(address owner_, address spender) external view override returns (uint256) { return _allowances[owner_][spender]; } //-------------------------------------------------------------------------- - // ButtonWrapper view methods + // ButtonWrapper View Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Returns the total underlying tokens held by this contract + * @dev Equivalent to the ERC20 balance of underlying tokens in this contract + * @return The total underlying token balance + */ function totalUnderlying() external view override returns (uint256) { return _bitsToUAmount(_activeBits()); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Returns the underlying token balance attributable to an account + * @dev This is the amount of underlying tokens the user could withdraw + * + * @param who The address to query + * @return The underlying token balance + * + * @custom:example + * ```solidity + * uint256 underlying = buttonToken.balanceOfUnderlying(msg.sender); + * // This value stays constant regardless of price changes + * ``` + */ function balanceOfUnderlying(address who) external view override returns (uint256) { if (who == address(0)) { return 0; @@ -242,14 +408,40 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return _bitsToUAmount(_accountBits[who]); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Converts underlying token amount to button token amount + * @dev Useful for calculating expected button tokens from a deposit + * + * @param uAmount The amount of underlying tokens + * @return The equivalent amount of button tokens at current price + * + * @custom:example + * ```solidity + * uint256 expectedButtons = buttonToken.underlyingToWrapper(1 ether); + * // If price is $2000, returns 2000e18 (2000 button tokens) + * ``` + */ function underlyingToWrapper(uint256 uAmount) external view override returns (uint256) { uint256 price; (price, ) = _queryPrice(); return _bitsToAmount(_uAmountToBits(uAmount), price); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Converts button token amount to underlying token amount + * @dev Useful for calculating underlying tokens received from a burn + * + * @param amount The amount of button tokens + * @return The equivalent amount of underlying tokens at current price + * + * @custom:example + * ```solidity + * uint256 underlying = buttonToken.wrapperToUnderlying(2000e18); + * // If price is $2000, returns 1e18 (1 underlying token) + * ``` + */ function wrapperToUnderlying(uint256 amount) external view override returns (uint256) { uint256 price; (price, ) = _queryPrice(); @@ -257,9 +449,24 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } //-------------------------------------------------------------------------- - // ERC-20 write methods + // ERC-20 Write Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Transfers button tokens to a recipient + * @dev Triggers rebase before transfer. Amount is in button tokens (rebased) + * + * @param to The recipient address + * @param amount The amount of button tokens to transfer + * @return success True if transfer succeeded + * + * Requirements: + * - to cannot be zero address or this contract + * - Caller must have sufficient balance + * + * @custom:emits Transfer(from, to, amount) + */ function transfer( address to, uint256 amount @@ -268,7 +475,20 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - /// @inheritdoc IRebasingERC20 + /** + * @inheritdoc IRebasingERC20 + * @notice Transfers the entire button token balance to a recipient + * @dev Useful for avoiding dust from rounding errors + * + * @param to The recipient address + * @return success True if transfer succeeded + * + * @custom:example + * ```solidity + * // Transfer 100% of balance, avoiding rounding dust + * buttonToken.transferAll(recipient); + * ``` + */ function transferAll( address to ) external override validRecipient(to) onAfterRebase returns (bool) { @@ -277,7 +497,22 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Transfers button tokens from one address to another + * @dev Requires approval. Supports infinite approval (type(uint256).max) + * + * @param from The sender address + * @param to The recipient address + * @param amount The amount of button tokens to transfer + * @return success True if transfer succeeded + * + * Requirements: + * - from must have sufficient balance + * - Caller must have sufficient allowance (or infinite approval) + * + * @custom:note Infinite approvals (type(uint256).max) are not decremented + */ function transferFrom( address from, address to, @@ -292,7 +527,15 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - /// @inheritdoc IRebasingERC20 + /** + * @inheritdoc IRebasingERC20 + * @notice Transfers the entire balance from one address to another + * @dev Requires approval for at least the sender's full balance + * + * @param from The sender address + * @param to The recipient address + * @return success True if transfer succeeded + */ function transferAllFrom( address from, address to @@ -309,7 +552,18 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - /// @inheritdoc IERC20 + /** + * @inheritdoc IERC20 + * @notice Approves a spender to transfer button tokens + * @dev Set to type(uint256).max for infinite approval + * + * @param spender The address to approve + * @param amount The maximum amount to approve + * @return success True if approval succeeded + * + * @custom:security Be careful with approvals, prefer increaseAllowance + * @custom:emits Approval(owner, spender, amount) + */ function approve(address spender, uint256 amount) external override returns (bool) { _allowances[_msgSender()][spender] = amount; @@ -317,7 +571,14 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - // @inheritdoc IERC20 + /** + * @notice Increases the allowance for a spender + * @dev Safer than approve() for increasing allowance + * + * @param spender The address to increase allowance for + * @param addedAmount The amount to add to the current allowance + * @return success True if operation succeeded + */ function increaseAllowance(address spender, uint256 addedAmount) external returns (bool) { _allowances[_msgSender()][spender] += addedAmount; @@ -325,7 +586,14 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return true; } - // @inheritdoc IERC20 + /** + * @notice Decreases the allowance for a spender + * @dev Saturates at zero (won't underflow) + * + * @param spender The address to decrease allowance for + * @param subtractedAmount The amount to subtract from current allowance + * @return success True if operation succeeded + */ function decreaseAllowance(address spender, uint256 subtractedAmount) external returns (bool) { if (subtractedAmount >= _allowances[_msgSender()][spender]) { delete _allowances[_msgSender()][spender]; @@ -338,17 +606,44 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } //-------------------------------------------------------------------------- - // RebasingERC20 write methods + // Rebasing Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IRebasingERC20 + /** + * @inheritdoc IRebasingERC20 + * @notice Triggers a manual rebase + * @dev Called automatically by most state-changing functions + * Can be called manually to update balances without a transfer + * + * @custom:emits Rebase(epoch, newPrice) if price changed + */ function rebase() external override onAfterRebase { return; } //-------------------------------------------------------------------------- - // ButtonWrapper write methods + // ButtonWrapper Write Functions - Minting + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Mints a specific amount of button tokens + * @dev Pulls the required underlying tokens automatically + * + * @param amount The desired amount of button tokens to mint + * @return uAmount The amount of underlying tokens deposited + * + * Requirements: + * - Caller must have approved this contract for underlying tokens + * - Resulting underlying deposit must be > 0 + * + * @custom:example + * ```solidity + * // Mint exactly 2000 button tokens + * underlying.approve(address(buttonToken), type(uint256).max); + * uint256 underlyingUsed = buttonToken.mint(2000e18); + * ``` + */ function mint(uint256 amount) external override onAfterRebase returns (uint256) { uint256 bits = _amountToBits(amount, lastPrice); uint256 uAmount = _bitsToUAmount(bits); @@ -356,7 +651,15 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Mints button tokens to a specified recipient + * @dev Caller provides underlying, recipient receives button tokens + * + * @param to The recipient of the minted button tokens + * @param amount The desired amount of button tokens to mint + * @return uAmount The amount of underlying tokens deposited + */ function mintFor(address to, uint256 amount) external override onAfterRebase returns (uint256) { uint256 bits = _amountToBits(amount, lastPrice); uint256 uAmount = _bitsToUAmount(bits); @@ -364,7 +667,24 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Burning + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Burns button tokens and returns underlying + * @dev Calculates underlying amount from button token amount + * + * @param amount The amount of button tokens to burn + * @return uAmount The amount of underlying tokens returned + * + * @custom:example + * ```solidity + * // Burn 2000 button tokens, receive underlying + * uint256 underlyingReceived = buttonToken.burn(2000e18); + * ``` + */ function burn(uint256 amount) external override onAfterRebase returns (uint256) { uint256 bits = _amountToBits(amount, lastPrice); uint256 uAmount = _bitsToUAmount(bits); @@ -372,7 +692,14 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns button tokens and sends underlying to recipient + * + * @param to The recipient of the underlying tokens + * @param amount The amount of button tokens to burn + * @return uAmount The amount of underlying tokens returned + */ function burnTo(address to, uint256 amount) external override onAfterRebase returns (uint256) { uint256 bits = _amountToBits(amount, lastPrice); uint256 uAmount = _bitsToUAmount(bits); @@ -380,7 +707,13 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns all button tokens and returns underlying + * @dev Use this to avoid dust from rounding + * + * @return uAmount The amount of underlying tokens returned + */ function burnAll() external override onAfterRebase returns (uint256) { uint256 bits = _accountBits[_msgSender()]; uint256 uAmount = _bitsToUAmount(bits); @@ -389,7 +722,13 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns all button tokens and sends underlying to recipient + * + * @param to The recipient of the underlying tokens + * @return uAmount The amount of underlying tokens returned + */ function burnAllTo(address to) external override onAfterRebase returns (uint256) { uint256 bits = _accountBits[_msgSender()]; uint256 uAmount = _bitsToUAmount(bits); @@ -398,7 +737,29 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Depositing + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Deposits underlying tokens and mints button tokens + * @dev This is the primary deposit function + * + * @param uAmount The amount of underlying tokens to deposit + * @return amount The amount of button tokens minted + * + * Requirements: + * - Caller must have approved this contract for underlying tokens + * - uAmount must be > 0 + * + * @custom:example + * ```solidity + * // Deposit 1 WETH, receive button tokens based on price + * weth.approve(address(buttonToken), 1 ether); + * uint256 buttonsReceived = buttonToken.deposit(1 ether); + * ``` + */ function deposit(uint256 uAmount) external override onAfterRebase returns (uint256) { uint256 bits = _uAmountToBits(uAmount); uint256 amount = _bitsToAmount(bits, lastPrice); @@ -406,7 +767,15 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Deposits underlying and mints button tokens to recipient + * @dev Caller provides underlying, recipient receives button tokens + * + * @param to The recipient of the minted button tokens + * @param uAmount The amount of underlying tokens to deposit + * @return amount The amount of button tokens minted + */ function depositFor( address to, uint256 uAmount @@ -417,7 +786,17 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Withdrawing + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws underlying tokens by specifying underlying amount + * + * @param uAmount The amount of underlying tokens to withdraw + * @return amount The amount of button tokens burned + */ function withdraw(uint256 uAmount) external override onAfterRebase returns (uint256) { uint256 bits = _uAmountToBits(uAmount); uint256 amount = _bitsToAmount(bits, lastPrice); @@ -425,7 +804,14 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws underlying tokens to a specified recipient + * + * @param to The recipient of the underlying tokens + * @param uAmount The amount of underlying tokens to withdraw + * @return amount The amount of button tokens burned + */ function withdrawTo( address to, uint256 uAmount @@ -436,7 +822,13 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws all underlying tokens + * @dev Use this to avoid dust from rounding + * + * @return amount The amount of button tokens burned + */ function withdrawAll() external override onAfterRebase returns (uint256) { uint256 bits = _accountBits[_msgSender()]; uint256 uAmount = _bitsToUAmount(bits); @@ -445,7 +837,13 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws all underlying tokens to a specified recipient + * + * @param to The recipient of the underlying tokens + * @return amount The amount of button tokens burned + */ function withdrawAllTo(address to) external override onAfterRebase returns (uint256) { uint256 bits = _accountBits[_msgSender()]; uint256 uAmount = _bitsToUAmount(bits); @@ -455,10 +853,19 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } //-------------------------------------------------------------------------- - // Private methods + // Private Functions + //-------------------------------------------------------------------------- - /// @dev Internal method to commit deposit state. - /// NOTE: Expects bits, uAmount, amount to be pre-calculated. + /** + * @dev Internal method to commit deposit state + * @param from Address providing underlying tokens + * @param to Address receiving button tokens + * @param uAmount Amount of underlying tokens + * @param amount Amount of button tokens + * @param bits Internal bit representation + * + * NOTE: Expects bits, uAmount, amount to be pre-calculated + */ function _deposit( address from, address to, @@ -474,8 +881,16 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { _transfer(address(0), to, bits, amount); } - /// @dev Internal method to commit withdraw state. - /// NOTE: Expects bits, uAmount, amount to be pre-calculated. + /** + * @dev Internal method to commit withdraw state + * @param from Address burning button tokens + * @param to Address receiving underlying tokens + * @param uAmount Amount of underlying tokens + * @param amount Amount of button tokens + * @param bits Internal bit representation + * + * NOTE: Expects bits, uAmount, amount to be pre-calculated + */ function _withdraw( address from, address to, @@ -490,8 +905,15 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { IERC20(underlying).safeTransfer(to, uAmount); } - /// @dev Internal method to commit transfer state. - /// NOTE: Expects bits/amounts to be pre-calculated. + /** + * @dev Internal method to commit transfer state + * @param from Source address (address(0) for minting) + * @param to Destination address (address(0) for burning) + * @param bits Amount in internal bits + * @param amount Amount in button tokens (for event) + * + * NOTE: Expects bits/amounts to be pre-calculated + */ function _transfer(address from, address to, uint256 bits, uint256 amount) private { _accountBits[from] -= bits; _accountBits[to] += bits; @@ -503,7 +925,16 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { } } - /// @dev Updates the `lastPrice` and recomputes the internal scalar. + /** + * @dev Updates the `lastPrice` and recomputes the internal scalar + * @param price The new price from the oracle + * + * Effects: + * - Caps price at maxPrice to prevent overflow + * - Updates lastPrice + * - Increments epoch counter + * - Emits Rebase event + */ function _rebase(uint256 price) private { uint256 _maxPrice = maxPrice; if (price > _maxPrice) { @@ -517,14 +948,22 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { emit Rebase(_epoch, price); } - /// @dev Returns the active "un-mined" bits + /** + * @dev Returns the active "un-mined" bits + * @return The total bits in circulation (not held by address(0)) + */ function _activeBits() private view returns (uint256) { return TOTAL_BITS - _accountBits[address(0)]; } - /// @dev Queries the oracle for the latest price - /// If fetched oracle price isn't valid returns the last price, - /// else returns the new price from the oracle. + /** + * @dev Queries the oracle for the latest price + * @return newPrice The price to use (either fresh or cached) + * @return valid Whether the oracle returned valid data + * + * NOTE: Returns lastPrice if oracle data is invalid or price is 0 + * Price of 0 is invalid because it would cause division by zero + */ function _queryPrice() private view returns (uint256, bool) { uint256 newPrice; bool valid; @@ -535,32 +974,60 @@ contract ButtonToken is IButtonToken, Initializable, OwnableUpgradeable { return (valid && newPrice > 0 ? newPrice : lastPrice, valid && newPrice > 0); } - /// @dev Convert button token amount to bits. + /** + * @dev Convert button token amount to bits + * @param amount Amount in button tokens + * @param price Current price + * @return bits Internal bit representation + */ function _amountToBits(uint256 amount, uint256 price) private view returns (uint256) { return amount * _bitsPerToken(price); } - /// @dev Convert underlying token amount to bits. + /** + * @dev Convert underlying token amount to bits + * @param uAmount Amount in underlying tokens + * @return bits Internal bit representation + */ function _uAmountToBits(uint256 uAmount) private pure returns (uint256) { return uAmount * BITS_PER_UNDERLYING; } - /// @dev Convert bits to button token amount. + /** + * @dev Convert bits to button token amount + * @param bits Internal bit representation + * @param price Current price + * @return amount Amount in button tokens + */ function _bitsToAmount(uint256 bits, uint256 price) private view returns (uint256) { return bits / _bitsPerToken(price); } - /// @dev Convert bits to underlying token amount. + /** + * @dev Convert bits to underlying token amount + * @param bits Internal bit representation + * @return uAmount Amount in underlying tokens + */ function _bitsToUAmount(uint256 bits) private pure returns (uint256) { return bits / BITS_PER_UNDERLYING; } - /// @dev Internal scalar to convert bits to button tokens. + /** + * @dev Internal scalar to convert bits to button tokens + * @param price Current price from oracle + * @return Bits per button token at the given price + */ function _bitsPerToken(uint256 price) private view returns (uint256) { return priceBits / price; } - /// @dev Derives max-price based on price-decimals + /** + * @dev Derives max-price based on price-decimals + * @param priceDecimals Number of decimals in the price feed + * @return maxPrice Maximum safe price value for the given decimals + * + * NOTE: Optimized for common decimal values (18, 8, 6) + */ function maxPriceFromPriceDecimals(uint256 priceDecimals) private pure returns (uint256) { require(priceDecimals <= 18, "ButtonToken: Price Decimals must be under 18"); // Given that 18,8,6 are the most common price decimals, we optimize for those cases diff --git a/contracts/UnbuttonToken.sol b/contracts/UnbuttonToken.sol index 9b73de8..6f2346a 100644 --- a/contracts/UnbuttonToken.sol +++ b/contracts/UnbuttonToken.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.4; import {IButtonWrapper} from "./interfaces/IButtonWrapper.sol"; @@ -8,53 +9,120 @@ import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ER import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; /** - * @title The UnbuttonToken ERC20 wrapper. + * @title UnbuttonToken - Fixed Balance ERC20 Token Wrapper + * @author Buttonwood Protocol + * @notice Wraps elastic/rebasing tokens into fixed-balance tokens using a share-based model * - * @dev The UnbuttonToken wraps elastic balance (rebasing) tokens like - * AMPL, Chai and AAVE's aTokens, to create a fixed balance representation. + * @dev The UnbuttonToken wraps elastic balance (rebasing) tokens like AMPL, Chai, + * and AAVE's aTokens, to create a fixed balance representation. * - * The ratio of a user’s balance to the total supply represents - * their share of the total deposit pool. + * ## How It Works * + * The ratio of a user's balance to the total supply represents their share + * of the total deposit pool. As the underlying rebasing token changes its + * supply, the user's UnbuttonToken balance stays the same, but their + * proportional claim to the underlying pool remains constant. + * + * **Example Scenario:** + * 1. Alice deposits 1,000 AMPL when total pool is 10,000 AMPL + * 2. Alice receives UnbuttonAMPL tokens representing 10% of the pool + * 3. AMPL rebases and the pool grows to 12,000 AMPL + * 4. Alice's UnbuttonAMPL balance is unchanged + * 5. Alice can now withdraw 1,200 AMPL (still 10% of the pool) + * + * ## Use Cases + * + * - **DeFi Integration**: Makes rebasing tokens compatible with protocols + * that expect fixed balances (lending, AMMs, etc.) + * - **Portfolio Management**: Simplifies accounting for rebasing assets + * - **Yield Farming**: Enables rebasing tokens in yield aggregators + * + * ## Mathematical Model + * + * - `userShares = userDeposit * totalSupply / totalUnderlying` + * - `userUnderlying = userShares * totalUnderlying / totalSupply` + * + * ## Security Considerations + * + * The maximum units of the underlying token that can be safely deposited + * without numeric overflow is calculated as: + * + * `MAX_UNDERLYING = sqrt(MAX_UINT256 / INITIAL_RATE)` + * + * where INITIAL_RATE is the conversion between underlying tokens to + * unbutton tokens for the initial mint. + * + * Since underlying balances increase due to both: + * 1. Users depositing into this contract + * 2. The underlying token rebasing + * + * There's no way to absolutely ENFORCE this bound. In practice, + * the underlying of any token with a reasonable supply will never + * reach this limit. */ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; - //-------------------------------------------------------------------------- - // PLEASE READ BEFORE CHANGING ANY ACCOUNTING OR MATH - // The maximum units of the underlying token that can be - // safely deposited into this contract without any numeric overflow - // is calculated as: - // - // MAX_UNDERLYING = sqrt(MAX_UINT256/INITIAL_RATE) - // - // where INITIAL_RATE is the conversion between underlying tokens to unbutton tokens - // for the initial mint. - // - // Since the underlying balances increase due both users depositing - // into this contract as well as the underlying token rebasing, - // there's no way to absolutely ENFORCE this bound. - // - // In practice the underlying of any token with a reasonable supply - // will never be this high. //-------------------------------------------------------------------------- // Constants + //-------------------------------------------------------------------------- - /// @dev Small deposit which is locked to the contract to ensure that - /// the `totalUnderlying` balance is always non-zero. + /** + * @notice Small deposit locked to the contract to ensure totalUnderlying is always non-zero + * @dev This prevents division by zero in share calculations and ensures + * the first depositor cannot manipulate the share price + */ uint256 public constant INITIAL_DEPOSIT = 1_000; //-------------------------------------------------------------------------- - // Attributes + // State Variables + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @dev The address of the rebasing ERC-20 token being wrapped (e.g., AMPL, aToken) + */ address public override underlying; + //-------------------------------------------------------------------------- + // Initialization //-------------------------------------------------------------------------- - /// @param underlying_ The underlying ERC20 token address. - /// @param name_ The ERC20 name. - /// @param symbol_ The ERC20 symbol. + /** + * @notice Initializes the UnbuttonToken contract + * @dev Can only be called once. Sets up the token with underlying asset and initial rate. + * Makes an initial micro-deposit to prevent share price manipulation. + * + * @param underlying_ The address of the rebasing ERC20 token to wrap + * @param name_ The human-readable name for the unbutton token + * @param symbol_ The short symbol for the unbutton token + * @param initialRate The initial conversion rate (unbutton tokens per underlying) + * + * Requirements: + * - Caller must have approved this contract for at least INITIAL_DEPOSIT of underlying + * - underlying_ must be a valid ERC20 address + * + * Effects: + * - Transfers INITIAL_DEPOSIT from caller to this contract + * - Mints INITIAL_DEPOSIT * initialRate unbutton tokens to this contract + * - These initial tokens are permanently locked to prevent share manipulation + * + * @custom:example + * ```solidity + * // Approve the factory first + * ampl.approve(address(factory), 1000); + * + * // Create UnbuttonAMPL with 1:1 initial rate + * unbuttonToken.initialize( + * address(ampl), // AMPL token + * "Unbutton AMPL", // Name + * "ubAMPL", // Symbol + * 1 // Initial rate: 1 ubAMPL per AMPL + * ); + * ``` + * + * @custom:security Initial deposit prevents first-depositor front-running attacks + */ function initialize( address underlying_, string memory name_, @@ -66,6 +134,7 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { underlying = underlying_; // NOTE: First mint with initial micro deposit + // This ensures totalUnderlying() and totalSupply() are never zero uint256 mintAmount = INITIAL_DEPOSIT * initialRate; IERC20Upgradeable(underlying).safeTransferFrom( _msgSender(), @@ -76,37 +145,104 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { } //-------------------------------------------------------------------------- - // ButtonWrapper write methods + // ButtonWrapper Write Functions - Minting + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Mints a specific amount of unbutton tokens + * @dev Calculates and pulls the required underlying tokens automatically + * + * @param amount The desired amount of unbutton tokens to mint + * @return uAmount The amount of underlying tokens deposited + * + * Requirements: + * - Caller must have approved this contract for sufficient underlying + * - amount must result in uAmount > 0 after calculation + * + * @custom:formula uAmount = amount * totalUnderlying / totalSupply + * + * @custom:example + * ```solidity + * // Mint exactly 100 unbutton tokens + * ampl.approve(address(unbuttonToken), type(uint256).max); + * uint256 amplUsed = unbuttonToken.mint(100e18); + * ``` + */ function mint(uint256 amount) external override returns (uint256) { uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); _deposit(_msgSender(), _msgSender(), uAmount, amount); return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Mints unbutton tokens to a specified recipient + * @dev Caller provides underlying tokens, recipient receives unbutton tokens + * + * @param to The recipient of the minted unbutton tokens + * @param amount The desired amount of unbutton tokens to mint + * @return uAmount The amount of underlying tokens deposited + */ function mintFor(address to, uint256 amount) external override returns (uint256) { uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); _deposit(_msgSender(), to, uAmount, amount); return uAmount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Burning + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Burns unbutton tokens and returns underlying + * @dev Calculates underlying amount based on current share price + * + * @param amount The amount of unbutton tokens to burn + * @return uAmount The amount of underlying tokens returned + * + * @custom:formula uAmount = amount * totalUnderlying / totalSupply + * + * @custom:example + * ```solidity + * // Burn 100 unbutton tokens, receive underlying + * uint256 amplReceived = unbuttonToken.burn(100e18); + * ``` + */ function burn(uint256 amount) external override returns (uint256) { uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); _withdraw(_msgSender(), _msgSender(), uAmount, amount); return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns unbutton tokens and sends underlying to recipient + * + * @param to The recipient of the underlying tokens + * @param amount The amount of unbutton tokens to burn + * @return uAmount The amount of underlying tokens returned + */ function burnTo(address to, uint256 amount) external override returns (uint256) { uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); _withdraw(_msgSender(), to, uAmount, amount); return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns all unbutton tokens and returns underlying + * @dev Use this to withdraw 100% of your position without dust + * + * @return uAmount The amount of underlying tokens returned + * + * @custom:example + * ```solidity + * // Withdraw entire position + * uint256 totalAmpl = unbuttonToken.burnAll(); + * ``` + */ function burnAll() external override returns (uint256) { uint256 amount = balanceOf(_msgSender()); uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); @@ -114,7 +250,13 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Burns all unbutton tokens and sends underlying to recipient + * + * @param to The recipient of the underlying tokens + * @return uAmount The amount of underlying tokens returned + */ function burnAllTo(address to) external override returns (uint256) { uint256 amount = balanceOf(_msgSender()); uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); @@ -122,35 +264,93 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { return uAmount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Depositing + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Deposits underlying tokens and mints unbutton tokens + * @dev This is the primary deposit function + * + * @param uAmount The amount of underlying (rebasing) tokens to deposit + * @return amount The amount of unbutton tokens minted + * + * Requirements: + * - Caller must have approved this contract for uAmount of underlying + * - uAmount must be > 0 + * + * @custom:formula amount = uAmount * totalSupply / totalUnderlying + * + * @custom:example + * ```solidity + * // Deposit 1000 AMPL, receive unbutton tokens + * ampl.approve(address(unbuttonToken), 1000e9); + * uint256 sharesReceived = unbuttonToken.deposit(1000e9); + * ``` + */ function deposit(uint256 uAmount) external override returns (uint256) { uint256 amount = _fromUnderlyingAmount(uAmount, _queryUnderlyingBalance(), totalSupply()); _deposit(_msgSender(), _msgSender(), uAmount, amount); return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Deposits underlying and mints unbutton tokens to recipient + * @dev Caller provides underlying, recipient receives unbutton tokens + * + * @param to The recipient of the minted unbutton tokens + * @param uAmount The amount of underlying tokens to deposit + * @return amount The amount of unbutton tokens minted + */ function depositFor(address to, uint256 uAmount) external override returns (uint256) { uint256 amount = _fromUnderlyingAmount(uAmount, _queryUnderlyingBalance(), totalSupply()); _deposit(_msgSender(), to, uAmount, amount); return amount; } - /// @inheritdoc IButtonWrapper + //-------------------------------------------------------------------------- + // ButtonWrapper Write Functions - Withdrawing + //-------------------------------------------------------------------------- + + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws a specific amount of underlying tokens + * @dev Burns the required unbutton tokens automatically + * + * @param uAmount The amount of underlying tokens to withdraw + * @return amount The amount of unbutton tokens burned + * + * @custom:formula amount = uAmount * totalSupply / totalUnderlying + */ function withdraw(uint256 uAmount) external override returns (uint256) { uint256 amount = _fromUnderlyingAmount(uAmount, _queryUnderlyingBalance(), totalSupply()); _withdraw(_msgSender(), _msgSender(), uAmount, amount); return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws underlying tokens to a specified recipient + * + * @param to The recipient of the underlying tokens + * @param uAmount The amount of underlying tokens to withdraw + * @return amount The amount of unbutton tokens burned + */ function withdrawTo(address to, uint256 uAmount) external override returns (uint256) { uint256 amount = _fromUnderlyingAmount(uAmount, _queryUnderlyingBalance(), totalSupply()); _withdraw(_msgSender(), to, uAmount, amount); return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws all underlying tokens + * @dev Use this to withdraw 100% of your position without dust + * + * @return amount The amount of unbutton tokens burned + */ function withdrawAll() external override returns (uint256) { uint256 amount = balanceOf(_msgSender()); uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); @@ -158,7 +358,13 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { return amount; } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Withdraws all underlying tokens to a specified recipient + * + * @param to The recipient of the underlying tokens + * @return amount The amount of unbutton tokens burned + */ function withdrawAllTo(address to) external override returns (uint256) { uint256 amount = balanceOf(_msgSender()); uint256 uAmount = _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); @@ -167,33 +373,81 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { } //-------------------------------------------------------------------------- - // ButtonWrapper view methods + // ButtonWrapper View Functions + //-------------------------------------------------------------------------- - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Returns the total underlying tokens held by this contract + * @dev This value changes as the underlying rebasing token changes supply + * @return The current balance of underlying tokens in the contract + * + * @custom:note Value increases/decreases with rebases even without deposits + */ function totalUnderlying() external view override returns (uint256) { return _queryUnderlyingBalance(); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Returns the underlying token balance attributable to an account + * @dev Calculated as: userBalance * totalUnderlying / totalSupply + * + * @param owner The address to query + * @return The underlying token balance + * + * @custom:example + * ```solidity + * // Check how much AMPL you can withdraw + * uint256 myAmpl = unbuttonToken.balanceOfUnderlying(msg.sender); + * // This value changes as AMPL rebases! + * ``` + */ function balanceOfUnderlying(address owner) external view override returns (uint256) { return _toUnderlyingAmount(balanceOf(owner), _queryUnderlyingBalance(), totalSupply()); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Converts underlying token amount to unbutton token amount + * @dev Useful for calculating expected shares from a deposit + * + * @param uAmount The amount of underlying tokens + * @return The equivalent amount of unbutton tokens + * + * @custom:formula shares = uAmount * totalSupply / totalUnderlying + */ function underlyingToWrapper(uint256 uAmount) external view override returns (uint256) { return _fromUnderlyingAmount(uAmount, _queryUnderlyingBalance(), totalSupply()); } - /// @inheritdoc IButtonWrapper + /** + * @inheritdoc IButtonWrapper + * @notice Converts unbutton token amount to underlying token amount + * @dev Useful for calculating underlying tokens received from a burn + * + * @param amount The amount of unbutton tokens + * @return The equivalent amount of underlying tokens + * + * @custom:formula underlying = amount * totalUnderlying / totalSupply + */ function wrapperToUnderlying(uint256 amount) external view override returns (uint256) { return _toUnderlyingAmount(amount, _queryUnderlyingBalance(), totalSupply()); } //-------------------------------------------------------------------------- - // Private methods + // Private Functions + //-------------------------------------------------------------------------- - /// @dev Internal method to commit deposit state. - /// NOTE: Expects uAmount, amount to be pre-calculated. + /** + * @dev Internal method to commit deposit state + * @param from Address providing underlying tokens + * @param to Address receiving unbutton tokens + * @param uAmount Amount of underlying tokens + * @param amount Amount of unbutton tokens to mint + * + * NOTE: Expects uAmount, amount to be pre-calculated + */ function _deposit(address from, address to, uint256 uAmount, uint256 amount) private { require(amount > 0, "UnbuttonToken: too few unbutton tokens to mint"); @@ -204,8 +458,15 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { _mint(to, amount); } - /// @dev Internal method to commit deposit state. - /// NOTE: Expects uAmount, amount to be pre-calculated. + /** + * @dev Internal method to commit withdraw state + * @param from Address burning unbutton tokens + * @param to Address receiving underlying tokens + * @param uAmount Amount of underlying tokens + * @param amount Amount of unbutton tokens to burn + * + * NOTE: Expects uAmount, amount to be pre-calculated + */ function _withdraw(address from, address to, uint256 uAmount, uint256 amount) private { require(amount > 0, "UnbuttonToken: too few unbutton tokens to burn"); @@ -216,12 +477,25 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { IERC20Upgradeable(underlying).safeTransfer(to, uAmount); } - /// @dev Queries the underlying ERC-20 balance of this contract. + /** + * @dev Queries the underlying ERC-20 balance of this contract + * @return The current balance of underlying tokens held by this contract + * + * @custom:note This balance changes with rebases of the underlying token + */ function _queryUnderlyingBalance() private view returns (uint256) { return IERC20Upgradeable(underlying).balanceOf(address(this)); } - /// @dev Converts underlying to unbutton token amount. + /** + * @dev Converts underlying to unbutton token amount + * @param uAmount Amount of underlying tokens + * @param totalUnderlying_ Total underlying tokens in the contract + * @param totalSupply Total supply of unbutton tokens + * @return The equivalent amount of unbutton tokens + * + * @custom:formula shares = uAmount * totalSupply / totalUnderlying + */ function _fromUnderlyingAmount( uint256 uAmount, uint256 totalUnderlying_, @@ -230,7 +504,15 @@ contract UnbuttonToken is IButtonWrapper, ERC20PermitUpgradeable { return (uAmount * totalSupply) / totalUnderlying_; } - /// @dev Converts unbutton to underlying token amount. + /** + * @dev Converts unbutton to underlying token amount + * @param amount Amount of unbutton tokens + * @param totalUnderlying_ Total underlying tokens in the contract + * @param totalSupply Total supply of unbutton tokens + * @return The equivalent amount of underlying tokens + * + * @custom:formula underlying = amount * totalUnderlying / totalSupply + */ function _toUnderlyingAmount( uint256 amount, uint256 totalUnderlying_,