Skip to content

Commit

Permalink
Merge pull request #114 from GenerationSoftware/gen-1772-114-weth-has…
Browse files Browse the repository at this point in the history
…-automatic-yield-accrual-on-blast-that-is-not

Add blast-compatible prize pool extension
  • Loading branch information
trmid authored Jun 28, 2024
2 parents a5810c1 + a81ed51 commit 800c464
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 0 deletions.
1 change: 1 addition & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export MAINNET_RPC_URL=""
export ARBITRUM_RPC_URL=""
export OPTIMISM_RPC_URL=""
export POLYGON_RPC_URL=""
export BLAST_RPC_URL=""

# Testnet RPC URLs
export GOERLI_RPC_URL=""
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
- name: Run Forge test
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
run: |
forge test
id: test
Expand All @@ -42,6 +43,7 @@ jobs:
- name: Run Forge coverage
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
run: |
forge coverage --report lcov && lcov --remove lcov.info -o lcov.info 'test/*'
id: coverage
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mainnet = "${MAINNET_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"
blast = "${BLAST_RPC_URL}"

goerli = "${GOERLI_RPC_URL}"
arbitrum-goerli = "${ARBITRUM_GOERLI_RPC_URL}"
Expand Down
70 changes: 70 additions & 0 deletions src/extensions/BlastPrizePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { PrizePool, ConstructorParams } from "../PrizePool.sol";

// The rebasing WETH token on Blast
IERC20Rebasing constant WETH = IERC20Rebasing(0x4300000000000000000000000000000000000004);

/// @notice The Blast yield modes for WETH
enum YieldMode {
AUTOMATIC,
VOID,
CLAIMABLE
}

/// @notice The relevant interface for rebasing WETH on Blast
interface IERC20Rebasing {
function configure(YieldMode) external returns (uint256);
function claim(address recipient, uint256 amount) external returns (uint256);
function getClaimableAmount(address account) external view returns (uint256);
}

/// @notice Thrown if the prize token is not the expected token on Blast.
/// @param prizeToken The prize token address
/// @param expectedToken The expected token address
error PrizeTokenNotExpectedToken(address prizeToken, address expectedToken);

/// @notice Thrown if a yield donation is triggered when there is no claimable balance.
error NoClaimableBalance();

/// @title PoolTogether V5 Blast Prize Pool
/// @author G9 Software Inc.
/// @notice A modified prize pool that opts in to claimable WETH yield on Blast and allows anyone to trigger
/// a donation of the accrued yield to the prize pool.
contract BlastPrizePool is PrizePool {

/* ============ Constructor ============ */

/// @notice Constructs a new Blast Prize Pool.
/// @dev Reverts if the prize token is not the expected WETH token on Blast.
/// @param params A struct of constructor parameters
constructor(ConstructorParams memory params) PrizePool(params) {
if (address(params.prizeToken) != address(WETH)) {
revert PrizeTokenNotExpectedToken(address(params.prizeToken), address(WETH));
}

// Opt-in to claimable yield
WETH.configure(YieldMode.CLAIMABLE);
}

/* ============ External Functions ============ */

/// @notice Returns the claimable WETH yield balance for this contract
function claimableYieldBalance() external view returns (uint256) {
return WETH.getClaimableAmount(address(this));
}

/// @notice Claims the available WETH yield balance and donates it to the prize pool.
/// @return The amount claimed and donated.
function donateClaimableYield() external returns (uint256) {
uint256 _claimableYieldBalance = WETH.getClaimableAmount(address(this));
if (_claimableYieldBalance == 0) {
revert NoClaimableBalance();
}
WETH.claim(address(this), _claimableYieldBalance);
contributePrizeTokens(DONATOR, _claimableYieldBalance);
return _claimableYieldBalance;
}

}
115 changes: 115 additions & 0 deletions test/extensions/BlastPrizePool.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";

import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
import { BlastPrizePool, ConstructorParams, WETH, PrizeTokenNotExpectedToken, NoClaimableBalance } from "../../src/extensions/BlastPrizePool.sol";
import { IERC20 } from "../../src/PrizePool.sol";

contract BlastPrizePoolTest is Test {
BlastPrizePool prizePool;

address bob = makeAddr("bob");
address alice = makeAddr("alice");

address wethWhale = address(0x66714DB8F3397c767d0A602458B5b4E3C0FE7dd1);

TwabController twabController;
IERC20 prizeToken;
address drawManager;

uint256 TIER_SHARES = 100;
uint256 CANARY_SHARES = 5;
uint256 RESERVE_SHARES = 10;

uint24 grandPrizePeriodDraws = 365;
uint48 drawPeriodSeconds = 1 days;
uint24 drawTimeout;
uint48 firstDrawOpensAt;
uint8 initialNumberOfTiers = 4;
uint256 winningRandomNumber = 123456;
uint256 tierLiquidityUtilizationRate = 1e18;

uint256 blockNumber = 5213491;
uint256 blockTimestamp = 1719236797;

ConstructorParams params;

function setUp() public {
drawTimeout = 30;

vm.createSelectFork("blast", blockNumber);
vm.warp(blockTimestamp);

prizeToken = IERC20(address(WETH));
twabController = new TwabController(uint32(drawPeriodSeconds), uint32(blockTimestamp - 1 days));

firstDrawOpensAt = uint48(blockTimestamp + 1 days); // set draw start 1 day into future

drawManager = address(this);

params = ConstructorParams(
prizeToken,
twabController,
drawManager,
tierLiquidityUtilizationRate,
drawPeriodSeconds,
firstDrawOpensAt,
grandPrizePeriodDraws,
initialNumberOfTiers,
uint8(TIER_SHARES),
uint8(CANARY_SHARES),
uint8(RESERVE_SHARES),
drawTimeout
);

prizePool = new BlastPrizePool(params);
prizePool.setDrawManager(address(this));
}

function testWrongPrizeToken() public {
params.prizeToken = IERC20(address(1));
vm.expectRevert(abi.encodeWithSelector(PrizeTokenNotExpectedToken.selector, address(1), address(WETH)));
prizePool = new BlastPrizePool(params);
}

function testClaimableYield() public {
assertEq(IERC20(address(WETH)).balanceOf(address(prizePool)), 0);

// check balance
assertEq(prizePool.claimableYieldBalance(), 0);

// donate some tokens to the prize pool
vm.startPrank(wethWhale);
IERC20(address(WETH)).approve(address(prizePool), 1e18);
prizePool.donatePrizeTokens(1e18);
vm.stopPrank();
assertEq(prizePool.getDonatedBetween(1, 1), 1e18);

// deal some ETH to the WETH contract and call addValue
deal(address(WETH), 1e18 + address(WETH).balance);
vm.startPrank(address(0x4300000000000000000000000000000000000000)); // REPORTER
(bool success,) = address(WETH).call(abi.encodeWithSignature("addValue(uint256)", 0));
vm.stopPrank();
require(success, "addValue failed");

// check balance non-zero
uint256 claimable = prizePool.claimableYieldBalance();
assertGt(claimable, 0);

// trigger donation
vm.startPrank(alice);
uint256 donated = prizePool.donateClaimableYield();
vm.stopPrank();

assertEq(donated, claimable);
assertEq(prizePool.getDonatedBetween(1, 1), 1e18 + donated);
assertEq(prizePool.claimableYieldBalance(), 0);

// reverts on donation of zero balance
vm.expectRevert(abi.encodeWithSelector(NoClaimableBalance.selector));
prizePool.donateClaimableYield();
}

}

0 comments on commit 800c464

Please sign in to comment.