|
1 |
| -# LRT Developers |
| 1 | +# Creating a LRT Vault on Tangle |
2 | 2 |
|
3 |
| -The LRT Developers section provides insights into the tools and resources available for developers working with the Tangle Network. This includes the Gadget SDK and other tools that enable the creation and deployment of Blueprints for Actively Validated Services (AVS). |
| 3 | +## Overview |
4 | 4 |
|
5 |
| -## Key Resources |
6 |
| -- **Gadget SDK**: A powerful tool for building Blueprints. |
7 |
| -- **Documentation**: Comprehensive guides and tutorials for developers. |
| 5 | +This tutorial walks through creating a Liquid Restaking Token (LRT) Vault on Tangle Network using the reference implementation from the [tangle-lrt](https://github.com/tangle-network/lrt) repository. LRT vaults allow users to receive liquid tokens representing their staked assets while participating in Tangle's restaking mechanism. |
| 6 | + |
| 7 | +## Prerequisites |
| 8 | + |
| 9 | +- Basic knowledge of Solidity and EVM development |
| 10 | +- [Foundry](https://book.getfoundry.sh/) installed for smart contract development |
| 11 | +- MetaMask wallet connected to Tangle Network |
| 12 | +- Some test tokens for deployment (on testnet) |
| 13 | + |
| 14 | +Install Foundry: |
| 15 | +```bash |
| 16 | +curl -L https://foundry.paradigm.xyz | bash |
| 17 | +foundryup |
| 18 | +``` |
| 19 | + |
| 20 | +## Understanding the Components |
| 21 | + |
| 22 | +The Tangle Liquid Restaking implementation consists of the following key components: |
| 23 | + |
| 24 | +1. **Vault Contract (TangleLiquidRestakingVault)**: An ERC4626-compliant vault that: |
| 25 | + - Manages deposits and withdrawals |
| 26 | + - Implements reward distribution with index-based accounting |
| 27 | + - Handles delegation through the MultiAssetDelegation precompile |
| 28 | + - Provides liquid token representation of staked assets |
| 29 | + |
| 30 | +2. **MultiAssetDelegation Wrapper**: Interfaces with Tangle's MultiAssetDelegation precompile at `0x0000000000000000000000000000000000000822` |
| 31 | + |
| 32 | +Core features include: |
| 33 | +```solidity |
| 34 | +contract TangleLiquidRestakingVault is ERC4626, Owned, TangleMultiAssetDelegationWrapper { |
| 35 | + /// @notice Scale factor for reward index calculations |
| 36 | + uint256 private constant REWARD_FACTOR = 1e18; |
| 37 | + |
| 38 | + /// @notice Tracks user's reward snapshot for a token |
| 39 | + struct RewardSnapshot { |
| 40 | + uint256 rewardIndex; // Global index at snapshot creation |
| 41 | + uint256 timestamp; // When snapshot was created |
| 42 | + uint256 shareBalance; // User's share balance at snapshot |
| 43 | + uint256 lastRewardIndex; // Index in rewardTimestamps array |
| 44 | + uint256 pendingRewards; // Unclaimed rewards at snapshot |
| 45 | + } |
| 46 | +
|
| 47 | + /// @notice Operator address for delegation |
| 48 | + bytes32 public operator; |
| 49 | +
|
| 50 | + /// @notice Blueprint selection for delegation and exposure |
| 51 | + uint64[] public blueprintSelection; |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +## Step 1: Setting Up the Project |
| 56 | + |
| 57 | +First, clone the reference implementation: |
| 58 | + |
| 59 | +```bash |
| 60 | +git clone https://github.com/tangle-network/lrt |
| 61 | +cd lrt |
| 62 | +forge soldeer update -d |
| 63 | +``` |
| 64 | + |
| 65 | +## Step 2: Core Implementation Details |
| 66 | + |
| 67 | +### Reward Distribution System |
| 68 | + |
| 69 | +The vault implements a sophisticated reward distribution system using index-based accounting: |
| 70 | + |
| 71 | +```solidity |
| 72 | +/// @notice Calculate pending rewards for a user |
| 73 | +function _calculatePendingRewards(address user, address token) internal view returns (uint256) { |
| 74 | + RewardSnapshot memory snapshot = userSnapshots[user][token]; |
| 75 | + RewardData storage rData = rewardData[token]; |
| 76 | +
|
| 77 | + // Calculate rewards since last snapshot |
| 78 | + uint256 indexDelta = rData.index - snapshot.rewardIndex; |
| 79 | + // Use mulDivUp for final reward calculation to ensure no dust is left behind |
| 80 | + uint256 newRewards = snapshot.shareBalance.mulDivUp(indexDelta, REWARD_FACTOR); |
| 81 | + uint256 totalRewards = newRewards + snapshot.pendingRewards; |
| 82 | +
|
| 83 | + return totalRewards; |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +### Delegation Management |
| 88 | + |
| 89 | +The vault handles delegation through the MultiAssetDelegation precompile, managing deposits and withdrawals: |
| 90 | + |
| 91 | +```solidity |
| 92 | +/// @notice Deposit assets into vault and delegate |
| 93 | +function deposit(uint256 assets, address receiver) public override returns (uint256 shares) { |
| 94 | + shares = previewDeposit(assets); |
| 95 | + |
| 96 | + // Process reward snapshots |
| 97 | + if (rewardTokens.length > 0) { |
| 98 | + _updateAllRewardIndices(); |
| 99 | + // ... snapshot processing |
| 100 | + } |
| 101 | +
|
| 102 | + // Complete deposit |
| 103 | + super.deposit(assets, receiver); |
| 104 | +
|
| 105 | + // Deposit into MADs and delegate |
| 106 | + _deposit(assets); |
| 107 | + _delegate(operator, assets, blueprintSelection); |
| 108 | + |
| 109 | + return shares; |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +## Step 3: Testing the Implementation |
| 114 | + |
| 115 | +Create test files in the `test` directory using Foundry's Solidity testing framework. Here's an example test structure: |
| 116 | + |
| 117 | +```solidity |
| 118 | +// SPDX-License-Identifier: UNLICENSED |
| 119 | +pragma solidity ^0.8.20; |
| 120 | +
|
| 121 | +import "forge-std/Test.sol"; |
| 122 | +import "../src/TangleLiquidRestakingVault.sol"; |
| 123 | +
|
| 124 | +contract TangleLiquidRestakingVaultTest is Test { |
| 125 | + TangleLiquidRestakingVault vault; |
| 126 | + address baseToken; |
| 127 | + bytes32 operator; |
| 128 | + uint64[] blueprintSelection; |
| 129 | + |
| 130 | + function setUp() public { |
| 131 | + // Setup test environment |
| 132 | + baseToken = address(new ERC20("Test Token", "TEST")); |
| 133 | + operator = bytes32(uint256(1)); |
| 134 | + blueprintSelection = new uint64[](1); |
| 135 | + blueprintSelection[0] = 1; |
| 136 | + |
| 137 | + // Deploy vault |
| 138 | + vault = new TangleLiquidRestakingVault( |
| 139 | + baseToken, |
| 140 | + operator, |
| 141 | + blueprintSelection, |
| 142 | + MULTI_ASSET_DELEGATION_CONTRACT, |
| 143 | + "Liquid Restaked Test", |
| 144 | + "lrTEST" |
| 145 | + ); |
| 146 | + } |
| 147 | +
|
| 148 | + function testDeposit() public { |
| 149 | + uint256 amount = 1000e18; |
| 150 | + deal(baseToken, address(this), amount); |
| 151 | + |
| 152 | + ERC20(baseToken).approve(address(vault), amount); |
| 153 | + vault.deposit(amount, address(this)); |
| 154 | + |
| 155 | + assertEq(vault.balanceOf(address(this)), amount); |
| 156 | + } |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +Run the tests using Forge: |
| 161 | +```bash |
| 162 | +forge test -vv |
| 163 | +``` |
| 164 | + |
| 165 | +## Step 4: Deployment |
| 166 | + |
| 167 | +The vault can be deployed using Forge's deployment capabilities: |
| 168 | + |
| 169 | +1. Create a deployment script in `script/DeployVault.s.sol`: |
| 170 | + |
| 171 | +```solidity |
| 172 | +// SPDX-License-Identifier: UNLICENSED |
| 173 | +pragma solidity ^0.8.20; |
| 174 | +
|
| 175 | +import "forge-std/Script.sol"; |
| 176 | +import "../src/TangleLiquidRestakingVault.sol"; |
| 177 | +
|
| 178 | +contract DeployVault is Script { |
| 179 | + function run() external { |
| 180 | + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); |
| 181 | + vm.startBroadcast(deployerPrivateKey); |
| 182 | +
|
| 183 | + // Deploy vault |
| 184 | + bytes32 operator = bytes32(uint256(vm.envUint("OPERATOR_ID"))); |
| 185 | + uint64[] memory blueprintSelection = new uint64[](1); |
| 186 | + blueprintSelection[0] = uint64(vm.envUint("BLUEPRINT_ID")); |
| 187 | +
|
| 188 | + TangleLiquidRestakingVault vault = new TangleLiquidRestakingVault( |
| 189 | + vm.envAddress("BASE_TOKEN"), |
| 190 | + operator, |
| 191 | + blueprintSelection, |
| 192 | + MULTI_ASSET_DELEGATION_CONTRACT, |
| 193 | + "Liquid Restaked Token", |
| 194 | + "LRT" |
| 195 | + ); |
| 196 | +
|
| 197 | + vm.stopBroadcast(); |
| 198 | + } |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +2. Configure deployment variables in `.env`: |
| 203 | + |
| 204 | +```env |
| 205 | +TANGLE_RPC_URL="https://testnet-rpc.tangle.tools" # or mainnet |
| 206 | +PRIVATE_KEY="your-private-key" |
| 207 | +BASE_TOKEN="0x..." |
| 208 | +OPERATOR_ID="1" # your operator ID |
| 209 | +BLUEPRINT_ID="1" # your blueprint ID |
| 210 | +``` |
| 211 | + |
| 212 | +3. Deploy using Forge: |
| 213 | + |
| 214 | +```bash |
| 215 | +forge script script/DeployVault.s.sol:DeployVault --rpc-url $TANGLE_RPC_URL --broadcast |
| 216 | +``` |
| 217 | + |
| 218 | +## Step 5: Interacting with the Vault |
| 219 | + |
| 220 | +The vault exposes several key functions for user interaction: |
| 221 | + |
| 222 | +### Deposits and Withdrawals |
| 223 | + |
| 224 | +```solidity |
| 225 | +/// Schedule unstaking of assets |
| 226 | +function scheduleUnstake(uint256 assets) external { |
| 227 | + uint256 shares = previewWithdraw(assets); |
| 228 | + require(balanceOf[msg.sender] >= shares, "Insufficient shares"); |
| 229 | +
|
| 230 | + // Track scheduled unstake |
| 231 | + scheduledUnstakeAmount[msg.sender] += assets; |
| 232 | +
|
| 233 | + // Process snapshots to stop reward accrual |
| 234 | + _processRewardSnapshots(msg.sender, address(0), shares); |
| 235 | +
|
| 236 | + // Schedule unstake through wrapper |
| 237 | + _scheduleUnstake(operator, assets); |
| 238 | +} |
| 239 | +
|
| 240 | +/// Execute the unstake |
| 241 | +function executeUnstake() external { |
| 242 | + uint256 scheduled = scheduledUnstakeAmount[msg.sender]; |
| 243 | + if (scheduled == 0) revert NoScheduledAmount(); |
| 244 | +
|
| 245 | + // Execute unstake through wrapper |
| 246 | + _executeUnstake(); |
| 247 | +
|
| 248 | + // Update state tracking |
| 249 | + unstakeAmount[msg.sender] += scheduled; |
| 250 | + scheduledUnstakeAmount[msg.sender] = 0; |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### Reward Management |
| 255 | + |
| 256 | +```solidity |
| 257 | +/// Claim rewards for specified tokens |
| 258 | +function claimRewards(address user, address[] calldata tokens) external returns (uint256[] memory) { |
| 259 | + if (msg.sender != user) revert Unauthorized(); |
| 260 | + |
| 261 | + rewards = new uint256[](tokens.length); |
| 262 | + _updateAllRewardIndices(); |
| 263 | + |
| 264 | + for (uint256 i = 0; i < tokens.length; i++) { |
| 265 | + address token = tokens[i]; |
| 266 | + if (!rewardData[token].isValid) revert InvalidRewardToken(); |
| 267 | + rewards[i] = _claimReward(user, token); |
| 268 | + } |
| 269 | + |
| 270 | + return rewards; |
| 271 | +} |
| 272 | +``` |
| 273 | + |
| 274 | +## Security Considerations |
| 275 | + |
| 276 | +The vault implements several security measures: |
| 277 | + |
| 278 | +1. **Access Control**: Uses Solmate's `Owned` for admin functions |
| 279 | +2. **Reward Accounting**: Index-based accounting prevents double-claiming |
| 280 | +3. **Withdrawal Process**: Two-step withdrawal process with unstaking period |
| 281 | +4. **Math Safety**: Uses `FixedPointMathLib` for safe calculations |
| 282 | + |
| 283 | +Key security features from the contract: |
| 284 | +```solidity |
| 285 | +/// @notice Scale factor for reward index calculations |
| 286 | +uint256 private constant REWARD_FACTOR = 1e18; |
| 287 | +
|
| 288 | +/// @notice Uses mulDivUp for final reward calculation |
| 289 | +uint256 newRewards = snapshot.shareBalance.mulDivUp(indexDelta, REWARD_FACTOR); |
| 290 | +
|
| 291 | +/// @notice Validates withdrawal amounts |
| 292 | +if (scheduledWithdrawAmount[owner] < assets) revert NoScheduledAmount(); |
| 293 | +``` |
| 294 | + |
| 295 | +For more information on the MultiAssetDelegation precompile and its features, see the [precompile documentation](../../developers/precompiles/features/multi-asset-delegation.mdx). |
0 commit comments