|
| 1 | +// SPDX-License-Identifier: AGPLv3 |
| 2 | +pragma solidity ^0.8.23; |
| 3 | + |
| 4 | +import { Test } from "forge-std/Test.sol"; |
| 5 | +import { console } from "forge-std/console.sol"; |
| 6 | +import { |
| 7 | + SparkYieldBackend |
| 8 | +} from "../../../contracts/superfluid/SparkYieldBackend.sol"; |
| 9 | +import { |
| 10 | + IERC20, |
| 11 | + ISuperfluid |
| 12 | +} from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; |
| 13 | +import { SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; |
| 14 | +import { IERC4626 } from "@openzeppelin-v5/contracts/interfaces/IERC4626.sol"; |
| 15 | + |
| 16 | +/** |
| 17 | + * @title SparkYieldBackendForkTest |
| 18 | + * @notice Fork test for testing yield-related features with SparkYieldBackend on Base |
| 19 | + * @author Superfluid |
| 20 | + */ |
| 21 | +contract SparkYieldBackendForkTest is Test { |
| 22 | + address internal constant ALICE = address(0x420); |
| 23 | + address internal constant ADMIN = address(0xAAA); |
| 24 | + address internal constant SURPLUS_RECEIVER = 0xac808840f02c47C05507f48165d2222FF28EF4e1; // dao.superfluid.eth |
| 25 | + |
| 26 | + // Base network constants |
| 27 | + uint256 internal constant CHAIN_ID = 8453; |
| 28 | + string internal constant RPC_URL = "https://mainnet.base.org"; |
| 29 | + |
| 30 | + // Spark USDC Vault on Base (sUSDC) |
| 31 | + address internal constant SPARK_VAULT = 0x3128a0F7f0ea68E7B7c9B00AFa7E41045828e858; |
| 32 | + |
| 33 | + // Common tokens on Base |
| 34 | + address internal constant USDCx = 0xD04383398dD2426297da660F9CCA3d439AF9ce1b; |
| 35 | + address internal constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; |
| 36 | + |
| 37 | + |
| 38 | + SuperToken internal superToken; |
| 39 | + SparkYieldBackend internal sparkBackend; |
| 40 | + IERC20 internal underlyingToken; |
| 41 | + IERC4626 internal vault; |
| 42 | + |
| 43 | + function setUp() public { |
| 44 | + vm.createSelectFork(RPC_URL); |
| 45 | + assertEq(block.chainid, CHAIN_ID, "Chainid mismatch"); |
| 46 | + |
| 47 | + vault = IERC4626(SPARK_VAULT); |
| 48 | + underlyingToken = IERC20(USDC); |
| 49 | + |
| 50 | + superToken = SuperToken(USDCx); |
| 51 | + |
| 52 | + sparkBackend = new SparkYieldBackend(vault, SURPLUS_RECEIVER); |
| 53 | + |
| 54 | + assertEq( |
| 55 | + address(sparkBackend.ASSET_TOKEN()), |
| 56 | + USDC, |
| 57 | + "Asset token mismatch" |
| 58 | + ); |
| 59 | + assertEq(address(sparkBackend.VAULT()), SPARK_VAULT, "Vault mismatch"); |
| 60 | + |
| 61 | + // upgrade SuperToken to new logic (mocking upgrade to enable features if needed, |
| 62 | + // essentially ensuring we have a fresh state or compatible logic) |
| 63 | + // Note: SuperToken on Base might already be up to date, but we re-deploy logic for safety in test |
| 64 | + SuperToken newSuperTokenLogic = new SuperToken( |
| 65 | + ISuperfluid(superToken.getHost()), |
| 66 | + superToken.POOL_ADMIN_NFT() |
| 67 | + ); |
| 68 | + vm.startPrank(address(superToken.getHost())); |
| 69 | + superToken.updateCode(address(newSuperTokenLogic)); |
| 70 | + vm.stopPrank(); |
| 71 | + |
| 72 | + // designate an admin for the SuperToken |
| 73 | + vm.startPrank(address(superToken.getHost())); |
| 74 | + superToken.changeAdmin(ADMIN); |
| 75 | + vm.stopPrank(); |
| 76 | + |
| 77 | + // provide ALICE with underlying and let her approve for upgrade |
| 78 | + deal(USDC, ALICE, type(uint128).max); |
| 79 | + vm.startPrank(ALICE); |
| 80 | + IERC20(USDC).approve(address(superToken), type(uint256).max); |
| 81 | + } |
| 82 | + |
| 83 | + function _enableYieldBackend() public { |
| 84 | + vm.startPrank(ADMIN); |
| 85 | + superToken.enableYieldBackend(sparkBackend); |
| 86 | + vm.stopPrank(); |
| 87 | + } |
| 88 | + |
| 89 | + function _verifyInvariants() internal view { |
| 90 | + // underlyingBalance + vaultAssets >= superToken.supply() |
| 91 | + uint256 underlyingBalance = IERC20(USDC).balanceOf(address(superToken)); |
| 92 | + // vault balance is in shares, need to convert to assets |
| 93 | + uint256 vaultAssets = vault.convertToAssets( |
| 94 | + vault.balanceOf(address(superToken)) |
| 95 | + ); |
| 96 | + |
| 97 | + (uint256 superTokenNormalizedSupply, ) = superToken.toUnderlyingAmount( |
| 98 | + superToken.totalSupply() |
| 99 | + ); |
| 100 | + |
| 101 | + // We use approx because of potential rounding/yield accruing differently per block |
| 102 | + // But assets should be >= supply |
| 103 | + assertGe( |
| 104 | + underlyingBalance + vaultAssets, |
| 105 | + superTokenNormalizedSupply, |
| 106 | + "invariant failed: underlying + vaultAssets insufficient" |
| 107 | + ); |
| 108 | + } |
| 109 | + |
| 110 | + function testSparkBackendDeployment() public view { |
| 111 | + assertEq( |
| 112 | + address(sparkBackend.ASSET_TOKEN()), |
| 113 | + USDC, |
| 114 | + "Asset token should be USDC" |
| 115 | + ); |
| 116 | + assertEq( |
| 117 | + address(sparkBackend.VAULT()), |
| 118 | + SPARK_VAULT, |
| 119 | + "Vault address should match" |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + function testEnableYieldBackend() public { |
| 124 | + _enableYieldBackend(); |
| 125 | + |
| 126 | + assertEq( |
| 127 | + address(superToken.getYieldBackend()), |
| 128 | + address(sparkBackend), |
| 129 | + "Yield backend mismatch" |
| 130 | + ); |
| 131 | + |
| 132 | + // For new deposits, we need to upgrade |
| 133 | + uint256 amount = 100 * 1e18; |
| 134 | + vm.startPrank(ALICE); |
| 135 | + superToken.upgrade(amount); |
| 136 | + vm.stopPrank(); |
| 137 | + |
| 138 | + // the SuperToken should now have a zero USDC balance (all deposited) |
| 139 | + assertEq( |
| 140 | + IERC20(USDC).balanceOf(address(superToken)), |
| 141 | + 0, |
| 142 | + "USDC balance should be zero" |
| 143 | + ); |
| 144 | + |
| 145 | + // And non-zero vault balance |
| 146 | + assertGt( |
| 147 | + vault.balanceOf(address(superToken)), |
| 148 | + 0, |
| 149 | + "Vault share balance should be non-zero" |
| 150 | + ); |
| 151 | + |
| 152 | + _verifyInvariants(); |
| 153 | + } |
| 154 | + |
| 155 | + function testDisableYieldBackend() public { |
| 156 | + _enableYieldBackend(); |
| 157 | + |
| 158 | + // Deposit some funds first so we have something to withdraw |
| 159 | + vm.startPrank(ALICE); |
| 160 | + superToken.upgrade(100 * 1e18); |
| 161 | + vm.stopPrank(); |
| 162 | + |
| 163 | + vm.startPrank(ADMIN); |
| 164 | + superToken.disableYieldBackend(); |
| 165 | + vm.stopPrank(); |
| 166 | + assertEq( |
| 167 | + address(superToken.getYieldBackend()), |
| 168 | + address(0), |
| 169 | + "Yield backend mismatch" |
| 170 | + ); |
| 171 | + |
| 172 | + // the SuperToken should now have a non-zero USDC balance and a zero vault balance |
| 173 | + assertGt( |
| 174 | + IERC20(USDC).balanceOf(address(superToken)), |
| 175 | + 0, |
| 176 | + "USDC balance should be non-zero" |
| 177 | + ); |
| 178 | + assertEq( |
| 179 | + vault.balanceOf(address(superToken)), |
| 180 | + 0, |
| 181 | + "Vault balance should be zero" |
| 182 | + ); |
| 183 | + |
| 184 | + _verifyInvariants(); |
| 185 | + } |
| 186 | + |
| 187 | + function testUpgradeDowngrade() public { |
| 188 | + _enableYieldBackend(); |
| 189 | + |
| 190 | + uint256 vaultSharesBefore = vault.balanceOf(address(superToken)); |
| 191 | + uint256 amount = 100 * 1e18; // 100 USDCx |
| 192 | + |
| 193 | + vm.startPrank(ALICE); |
| 194 | + superToken.upgrade(amount); |
| 195 | + vm.stopPrank(); |
| 196 | + |
| 197 | + uint256 vaultSharesAfter = vault.balanceOf(address(superToken)); |
| 198 | + |
| 199 | + assertGt( |
| 200 | + vaultSharesAfter, |
| 201 | + vaultSharesBefore, |
| 202 | + "Vault shares should increase" |
| 203 | + ); |
| 204 | + |
| 205 | + // downgrade |
| 206 | + vm.startPrank(ALICE); |
| 207 | + superToken.downgrade(amount); |
| 208 | + vm.stopPrank(); |
| 209 | + |
| 210 | + uint256 vaultSharesFinal = vault.balanceOf(address(superToken)); |
| 211 | + assertLt( |
| 212 | + vaultSharesFinal, |
| 213 | + vaultSharesAfter, |
| 214 | + "Vault shares should decrease" |
| 215 | + ); |
| 216 | + |
| 217 | + _verifyInvariants(); |
| 218 | + } |
| 219 | + |
| 220 | + function testWithdrawSurplusFromYieldBackend() public { |
| 221 | + // simulate the SuperToken having a surplus of underlying from the start |
| 222 | + uint256 surplusAmount = 100 * 1e6; // 100 USDC |
| 223 | + deal(USDC, address(superToken), surplusAmount); |
| 224 | + |
| 225 | + _enableYieldBackend(); |
| 226 | + |
| 227 | + uint256 receiverBalanceBefore = IERC20(USDC).balanceOf( |
| 228 | + SURPLUS_RECEIVER |
| 229 | + ); |
| 230 | + |
| 231 | + vm.startPrank(ADMIN); |
| 232 | + superToken.withdrawSurplusFromYieldBackend(); |
| 233 | + vm.stopPrank(); |
| 234 | + |
| 235 | + uint256 receiverBalanceAfter = IERC20(USDC).balanceOf(SURPLUS_RECEIVER); |
| 236 | + |
| 237 | + console.log("Receiver balance before", receiverBalanceBefore); |
| 238 | + console.log("Receiver balance after", receiverBalanceAfter); |
| 239 | + console.log("Diff", receiverBalanceAfter - receiverBalanceBefore); |
| 240 | + |
| 241 | + assertGt( |
| 242 | + receiverBalanceAfter, |
| 243 | + receiverBalanceBefore, |
| 244 | + "Surplus should be withdrawn to receiver" |
| 245 | + ); |
| 246 | + _verifyInvariants(); |
| 247 | + } |
| 248 | +} |
0 commit comments