5/5
## Mastering Cross-Chain Rebase Tokens: A Comprehensive Recap This lesson recaps the journey of designing, building, and deploying a sophisticated cross-chain rebase token system. We'll revisit the core token mechanics, vault interactions, Cross-Chain Interoperability Protocol (CCIP) integration, deployment strategies, and rigorous testing, culminating in a successful live cross-chain transaction. ## Core Component: The Rebase Token (`RebaseToken.sol`) The foundation of our system is the `RebaseToken.sol` smart contract, an ERC20 token imbued with dynamic rebasing capabilities and access control. * **Inheritance and Access Control:** The `RebaseToken` leverages OpenZeppelin's battle-tested contracts, inheriting from `ERC20` for standard token functionality, `Ownable` for administrative control, and `AccessControl` for granular permissioning. ```solidity // src/RebaseToken.sol contract RebaseToken is ERC20, Ownable, AccessControl { // ... } ``` * **Dedicated Mint and Burn Role:** To manage token supply changes, a specific `MINT_AND_BURN_ROLE` is defined. The contract owner can grant this role to other trusted contracts, such as the `Vault` and the `RebaseTokenPool`, empowering them to mint new tokens or burn existing ones. ```solidity // src/RebaseToken.sol bytes32 public constant MINT_AND_BURN_ROLE = keccak256("MINT_AND_BURN_ROLE"); function grantMintAndBurnRole(address _account) external onlyOwner { _grantRole(MINT_AND_BURN_ROLE, _account); } ``` * **Global and User-Specific Interest Rates:** A global interest rate, `s_interestRate`, governs the base rate for newly minted tokens. This rate can only be adjusted downwards by the owner via the `setInterestRate` function, ensuring a predictable (or decreasing) inflation model. When tokens are minted, the minter receives this global rate as their individual `s_userInterestRate`. ```solidity // src/RebaseToken.sol uint256 public s_interestRate; mapping(address => uint256) public s_userInterestRate; mapping(address => uint256) private s_userLastUpdatedTimestamp; // Tracks the last time interest was accrued for a user // Error for setInterestRate error RebaseToken__InterestRateCanOnlyDecrease(uint256 currentInterestRate, uint256 newInterestRate); // Event for setInterestRate event InterestRateSet(uint256 newInterestRate); function setInterestRate(uint256 _newInterestRate) external onlyOwner { if (_newInterestRate >= s_interestRate) { revert RebaseToken__InterestRateCanOnlyDecrease(s_interestRate, _newInterestRate); } s_interestRate = _newInterestRate; emit InterestRateSet(_newInterestRate); } ``` * **Distinguishing Principal Balance:** The `principleBalanceOf(address _user)` function returns the underlying amount of tokens initially minted to or received by a user, excluding any interest accrued. This is achieved by calling `super.balanceOf(_user)`, which accesses the standard ERC20 balance tracking. * **Minting Mechanism:** The `mint(address _to, uint256 _amount, uint256 _userInterestRate)` function, restricted to entities with `MINT_AND_BURN_ROLE`, handles token creation. Crucially, it first calls the internal `_mintAccruedInterest(_to)` function to update the recipient's balance with any pending interest. Then, it sets the user's specific interest rate (`s_userInterestRate[_to]`) and finally mints the new principal amount. This ensures interest calculations are always based on the latest balance. ```solidity // src/RebaseToken.sol function mint(address _to, uint256 _amount, uint256 _userInterestRate) external onlyRole(MINT_AND_BURN_ROLE) { _mintAccruedInterest(_to); s_userInterestRate[_to] = _userInterestRate; s_userLastUpdatedTimestamp[_to] = block.timestamp; // Set last updated on mint _mint(_to, _amount); } ``` * **Burning Mechanism:** Similarly, the `burn(address _from, uint256 _amount)` function, also protected by `MINT_AND_BURN_ROLE`, first ensures any accrued interest for the user `_from` is minted via `_mintAccruedInterest(_from)`. After this update, the specified `_amount` of tokens is burned from their balance. ```solidity // src/RebaseToken.sol function burn(address _from, uint256 _amount) external onlyRole(MINT_AND_BURN_ROLE) { _mintAccruedInterest(_from); _burn(_from, _amount); } ``` * **Rebasing Balance Calculation:** The core rebasing logic is implemented by overriding the standard `balanceOf(address _user)` function. It calculates the user's current, interest-inclusive balance by taking their principal balance (`super.balanceOf(_user)`) and multiplying it by the interest accumulated since their last balance-affecting interaction. This accumulation is determined by `_calculateUserAccumulatedInterestSinceLastUpdate(_user)`. A `PRECISION_FACTOR` (e.g., 1e18) is used to handle fractional interest. ```solidity // src/RebaseToken.sol uint256 private constant PRECISION_FACTOR = 1e18; function balanceOf(address _user) public view override returns (uint256) { if (super.balanceOf(_user) == 0) { return 0; } return (super.balanceOf(_user) * _calculateUserAccumulatedInterestSinceLastUpdate(_user)) / PRECISION_FACTOR; } ``` * **Interest-Aware Transfers:** The `transfer(address _to, uint256 _amount)` function is also overridden to integrate rebasing. Before the actual transfer occurs, it calls `_mintAccruedInterest` for both the sender (`msg.sender`) and the recipient (`_to`). This ensures their balances are up-to-date. If the recipient had a zero balance prior to the transfer, their user-specific interest rate (`s_userInterestRate[_to]`) is initialized to the sender's interest rate, propagating the rate to new holders. * **Linear Interest Calculation:** The internal view function `_calculateUserAccumulatedInterestSinceLastUpdate(address _user)` computes the interest accrued linearly. It calculates the time elapsed since the user's `s_userLastUpdatedTimestamp` and applies their `s_userInterestRate`. The formula is `PRECISION_FACTOR + (userInterestRate * timeElapsed)`, effectively returning a multiplier for the principal balance. ```solidity // src/RebaseToken.sol function _calculateUserAccumulatedInterestSinceLastUpdate(address _user) internal view returns (uint256 linearInterest) { if (s_userLastUpdatedTimestamp[_user] == 0 || s_userLastUpdatedTimestamp[_user] == block.timestamp) { return PRECISION_FACTOR; // No time elapsed or first interaction } uint256 timeElapsed = block.timestamp - s_userLastUpdatedTimestamp[_user]; linearInterest = PRECISION_FACTOR + (s_userInterestRate[_user] * timeElapsed); } ``` * **Internal Interest Accrual (`_mintAccruedInterest`):** This crucial internal function, `_mintAccruedInterest(address _user)`, is invoked before any operation that might alter a user's balance (mint, burn, transfer). It calculates the interest owed to the user since their last update, mints these interest tokens directly to their account, and then updates `s_userLastUpdatedTimestamp[_user]` to the current `block.timestamp`. This ensures that interest is compounded on subsequent calculations and that reported balances are always current. * **Utility Getter Functions:** Standard getter functions such as `getInterestRate()` (for the global `s_interestRate`) and `getUserInterestRate(address _user)` (for `s_userInterestRate[_user]`) are provided for external contracts or UIs to query these important parameters. ## User Interaction: The Vault (`Vault.sol`) The `Vault.sol` contract serves as the primary interface for users to deposit collateral (ETH) and mint rebase tokens, or to redeem their rebase tokens for the underlying collateral. * **Depositing ETH:** Users can call the `deposit()` payable function, sending ETH along with the transaction. The vault then interacts with the `RebaseToken` contract to mint an equivalent amount of rebase tokens to the depositor (`msg.sender`). The interest rate applied for this mint operation is the current global interest rate fetched from the `RebaseToken` contract. ```solidity // src/Vault.sol // Assuming i_rebaseToken is an instance of RebaseToken // event Deposit(address indexed user, uint256 amount); function deposit() external payable { // Consider adding a RebaseToken__ZeroAmount() revert if msg.value == 0 uint256 interestRate = i_rebaseToken.getInterestRate(); i_rebaseToken.mint(msg.sender, msg.value, interestRate); emit Deposit(msg.sender, msg.value); } ``` * **Redeeming Rebase Tokens:** The `redeem(uint256 _amount)` function allows users to exchange their rebase tokens for ETH. The vault first burns the specified `_amount` of rebase tokens from the user's balance (after their interest is accrued by the token contract's burn function). If `_amount` is `type(uint256).max`, the entire balance of the user is redeemed. It then sends the corresponding amount of ETH back to the user. ```solidity // src/Vault.sol // Assuming i_rebaseToken is an instance of RebaseToken // error Vault__RedeemFailed(); // event Redeem(address indexed user, uint256 amount); function redeem(uint256 _amount) external { uint256 amountToRedeem = _amount; if (_amount == type(uint256).max) { amountToRedeem = i_rebaseToken.balanceOf(msg.sender); } // RebaseToken__ZeroAmount() revert if amountToRedeem == 0 i_rebaseToken.burn(msg.sender, amountToRedeem); (bool success, ) = payable(msg.sender).call{value: amountToRedeem}(""); if (!success) { revert Vault__RedeemFailed(); } emit Redeem(msg.sender, amountToRedeem); } ``` * **Receiving ETH:** A `receive() external payable {}` function is included, enabling the `Vault` contract to directly receive ETH through standard transfers, which could be utilized for purposes like receiving rewards or funding. ## Going Cross-Chain: CCIP Integration (`RebaseTokenPool.sol`) To enable the rebase token to traverse different blockchains, we integrate it with Chainlink's Cross-Chain Interoperability Protocol (CCIP) using a specialized token pool contract. * **The Token Pool Contract:** The `RebaseTokenPool.sol` contract inherits from Chainlink's `TokenPool.sol`, providing the necessary framework for CCIP interactions. This pool will manage the locking/burning of tokens on the source chain and the releasing/minting on the destination chain. ```solidity // src/RebaseTokenPool.sol // import {TokenPool} from "@chainlink/contracts-ccip/src/v0.8/tokenpool/TokenPool.sol"; contract RebaseTokenPool is TokenPool { // Constructor would take RebaseToken address, router address, etc. // ... } ``` * **Lock or Burn on Source Chain:** When tokens are sent cross-chain, the `_lockOrBurn(address _account, uint256 _amount)` internal function (called by CCIP router interaction) is triggered on the source chain's `RebaseTokenPool`. In our system, this function is configured to *burn* the tokens from the sender's account. A critical piece of data, the user's specific interest rate (`s_userInterestRate[_account]`), is ABI-encoded and included in the `destPoolData` field of the CCIP message. This ensures the user's unique interest rate is preserved and propagated to the destination chain. * **Release or Mint on Destination Chain:** Upon arrival of the CCIP message on the destination chain, the `_releaseOrMint(address _to, uint256 _amount, bytes memory _sourcePoolData)` internal function is called on the destination `RebaseTokenPool`. This function *mints* the corresponding amount of rebase tokens to the recipient. Crucially, it decodes the user's original interest rate from the `_sourcePoolData` (which was packed by the source pool) and uses this specific rate when calling the `RebaseToken`'s `mint` function. This maintains interest rate continuity for the user across chains. ## Streamlining Deployment with Foundry Scripts Foundry scripts were developed to automate the deployment, configuration, and interaction processes, enhancing efficiency and reducing manual error. * **`Deployer.s.sol`:** This script handles the initial deployment of the `RebaseToken`, `RebaseTokenPool`, and `Vault` contracts. Post-deployment, it also configures essential roles, such as setting CCIP admin roles on the pool and granting the `MINT_AND_BURN_ROLE` on the `RebaseToken` contract to both the `RebaseTokenPool` and the `Vault`. * **`ConfigurePools.s.sol`:** After the token pools are deployed on their respective chains, this script is used to configure them for inter-chain communication. This includes enabling CCIP lanes by setting supported destination chains and configuring rate limits for token transfers. * **`BridgeTokens.s.sol`:** This script facilitates the initiation of a cross-chain token transfer. It constructs the `Client.EVM2AnyMessage` struct required by CCIP, populating it with the token address, amount, receiver address on the destination chain, and any other necessary parameters. It then calls the `ccipSend` function on the CCIP Router contract to dispatch the message. ## Ensuring Robustness: Comprehensive Testing with Foundry Thorough testing is paramount for smart contract security and reliability. Foundry's testing framework was employed extensively. * **Rebase Token and Vault Tests (`RebaseToken.t.sol`):** Extensive unit and fuzz tests were written for the `RebaseToken` and `Vault` contracts. Fuzz testing, in particular, helps uncover edge cases by subjecting functions to a wide range of random inputs, ensuring the interest accrual, rebasing logic, minting, and burning operations behave as expected under diverse conditions. * **Cross-Chain Functionality Tests (`CrossChain.t.sol`):** To validate the end-to-end cross-chain transfer mechanism, fork tests were implemented. * **CCIP Local Simulation:** Chainlink's `CCIPLocalSimulatorFork.sol` contract was utilized. This powerful tool allows for the simulation of CCIP message passing between two locally forked blockchain environments, providing a high-fidelity testing ground for cross-chain interactions without deploying to live testnets. * **Testnet Forking:** Foundry's cheatcodes, specifically `vm.createSelectFork("sepolia-eth")` and `vm.createFork("arb-sepolia")`, were used to create local instances of the Ethereum Sepolia and Arbitrum Sepolia testnets. This allowed testing the CCIP integration against realistic chain states. ## Real-World Application: Deployment and Live Cross-Chain Transfer The culmination of development and testing was the deployment of the system to live testnets and the execution of a cross-chain token transfer. * **Automated Deployment Script (`bridgeToZkSync.sh`):** A shell script, `bridgeToZkSync.sh`, was crafted to automate the deployment process for the `RebaseToken` and `RebaseTokenPool` contracts onto both the Sepolia and zkSync Sepolia testnets. This script handled contract compilation, deployment transactions, and crucial post-deployment setup. ```bash # bridgeToZkSync.sh (illustrative commands) # ... # Compile contracts # forge build # ... # Deploy Rebase Token contract on ZkSync Sepolia ZKSYNC_REBASE_TOKEN_ADDRESS=$(forge create src/RebaseToken.sol:RebaseToken --rpc-url $ZKSYNC_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --constructor-args "MyRebaseToken" "MRT" $INITIAL_OWNER_ADDRESS $INITIAL_INTEREST_RATE) # ... # Deploy RebaseTokenPool on ZkSync Sepolia ZKSYNC_POOL_ADDRESS=$(forge create src/RebaseTokenPool.sol:RebaseTokenPool --rpc-url $ZKSYNC_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --constructor-args $ZKSYNC_REBASE_TOKEN_ADDRESS $CCIP_ROUTER_ZKSYNC) # ... # Set the permissions for the pool contract on ZkSync RebaseToken cast send $ZKSYNC_REBASE_TOKEN_ADDRESS "grantMintAndBurnRole(address)" $ZKSYNC_POOL_ADDRESS --rpc-url $ZKSYNC_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY # Similar steps for Sepolia deployment and configuration... # ... ``` * **CCIP Lane Configuration:** Following contract deployments, the script (or subsequent manual steps) managed the necessary CCIP configurations. This involved setting admin roles on the token pools and enabling the CCIP message lanes and token transfer lanes between the Sepolia and zkSync Sepolia deployments, specifying supported tokens and rate limits. * **Initiating the Cross-Chain Transfer:** The script then proceeded to initiate a cross-chain transfer of the rebase tokens, sending them from an account on Sepolia to an account on zkSync Sepolia, leveraging the deployed `RebaseTokenPool` and the CCIP Router. * **Verification via CCIP Explorer:** The success of the cross-chain transaction was confirmed using the Chainlink CCIP Explorer (explorer.chain.link). This tool provided visibility into the message status, confirming its finalization on the destination chain (zkSync Sepolia). Importantly, it verified that the correct amount of tokens, inclusive of any accrued interest based on the sender's original interest rate, was minted to the recipient on zkSync Sepolia, validating the entire system's design and implementation. This comprehensive process, from foundational smart contract architecture to live testnet operations, demonstrates a robust methodology for building and validating complex cross-chain Web3 applications.
An architectural review of Mastering Cross-Chain Rebase Tokens: A Comprehensive Recap - Revisit the complete architecture of a cross-chain rebase token, including `RebaseToken` mechanics, `Vault` interactions, and CCIP integration. Discover how Foundry scripts automate deployment and testing, culminating in a live cross-chain transaction.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on May 20, 2025
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on May 20, 2025