Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add blast-compatible prize pool extension #114

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}

}
Loading