Skip to content

Commit 61b9d84

Browse files
committed
adjust hook logic to confidently emit an event when an NFT holder wins a prize
1 parent 3d1abc6 commit 61b9d84

File tree

2 files changed

+81
-26
lines changed

2 files changed

+81
-26
lines changed

src/prize-hooks/examples/nft-chance-booster/NftChanceBoosterHook.sol

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.24;
33

44
import { IPrizeHooks } from "pt-v5-vault/interfaces/IPrizeHooks.sol";
55
import { IERC721 } from "openzeppelin-v5/token/ERC721/IERC721.sol";
6-
import { PrizePool, TwabController } from "pt-v5-prize-pool/PrizePool.sol";
6+
import { PrizePool, TwabController, SafeERC20, IERC20 } from "pt-v5-prize-pool/PrizePool.sol";
77
import { UniformRandomNumber } from "uniform-random-number/UniformRandomNumber.sol";
88

99
uint256 constant PICK_GAS_ESTIMATE = 60_000; // probably lower, but we set it higher to avoid a reversion
@@ -30,12 +30,23 @@ error InvalidTokenIdBounds(uint256 tokenIdLowerBound, uint256 tokenIdUpperBound)
3030
/// @dev This contract works best with NFTs that have iterating IDs ex. IDs: (1,2,3,4,5,...)
3131
/// @author G9 Software Inc.
3232
contract NftChanceBoosterHook is IPrizeHooks {
33+
using SafeERC20 for IERC20;
3334

3435
/// @notice Emitted when a vault is boosted with a prize re-contribution.
3536
/// @param prizePool The prize pool the vault was boosted on
3637
/// @param boostedVault The boosted vault
3738
/// @param prizeAmount The amount of prize tokens contributed
38-
event BoostedVaultWithPrize(address indexed prizePool, address indexed boostedVault, uint256 prizeAmount);
39+
/// @param pickAttempts The number of times the hook tried to pick a winner
40+
event BoostedVaultWithPrize(address indexed prizePool, address indexed boostedVault, uint256 prizeAmount, uint256 pickAttempts);
41+
42+
/// @notice Emitted when an NFT holder wins a prize.
43+
/// @param nftWinner The NFT holder that won the prize
44+
/// @param vault The vault that the prize was won through
45+
/// @param donor The original winner of the prize before it was redirected
46+
/// @param tier The prize tier
47+
/// @param prizeIndex The prize index
48+
/// @param prizeAmount The amount of prize tokens won
49+
event PrizeWonByNftHolder(address indexed nftWinner, address indexed vault, address indexed donor, uint8 tier, uint32 prizeIndex, uint256 prizeAmount);
3950

4051
/// @notice The ERC721 token whose holders will have a chance to win prizes
4152
IERC721 public immutable nftCollection;
@@ -63,7 +74,7 @@ contract NftChanceBoosterHook is IPrizeHooks {
6374
/// @param prizePool_ The prize pool that is awarding prizes
6475
/// @param boostedVault_ The The vault that is being boosted
6576
/// @param minTwabOverPrizePeriod_ The minimum TWAB that the selected winner must have over the prize
66-
/// period to win the prize; if set to zero, no balance is needed.
77+
/// period to win the prize; if set to zero, no balance is needed.
6778
/// @param tokenIdLowerBound_ The lower bound of eligible NFT IDs (inclusive)
6879
/// @param tokenIdUpperBound_ The upper bound of eligible NFT IDs (inclusive)
6980
constructor(
@@ -90,14 +101,15 @@ contract NftChanceBoosterHook is IPrizeHooks {
90101

91102
/// @inheritdoc IPrizeHooks
92103
/// @dev This prize hook uses the random number from the last awarded prize pool draw to randomly select
93-
/// the receiver of the prize from a list of current NFT holders. The prize tier and prize index are also
94-
/// used to provide variance in the entropy for each prize so there can be multiple winners per draw.
104+
/// the receiver of the prize from a list of current NFT holders. The prize data is also used to provide
105+
/// variance in the entropy for each prize so there can be multiple winners per draw.
95106
/// @dev Tries to select a winner until the call runs out of gas before reverting to the backup action of
96107
/// contributing the prize on behalf of the boosted vault.
108+
/// @dev Returns the winning token holder in data if successful or the number of pick attempts if not successful.
97109
function beforeClaimPrize(address _winner, uint8 _tier, uint32 _prizeIndex, uint96, address) external view returns (address, bytes memory) {
98110
uint256 _tierStartTime;
99111
uint256 _tierEndTime;
100-
uint256 _winningRandomNumber = prizePool.getWinningRandomNumber();
112+
bytes32 _entropy = keccak256(abi.encode(prizePool.getWinningRandomNumber(), msg.sender, _winner, _tier, _prizeIndex));
101113
{
102114
uint24 _tierEndDrawId = prizePool.getLastAwardedDrawId();
103115
uint24 _tierStartDrawId = prizePool.computeRangeStartDrawIdInclusive(
@@ -117,7 +129,7 @@ contract NftChanceBoosterHook is IPrizeHooks {
117129
_randomTokenId = tokenIdLowerBound;
118130
} else {
119131
_randomTokenId = tokenIdLowerBound + UniformRandomNumber.uniform(
120-
uint256(keccak256(abi.encode(_winningRandomNumber, _winner, _tier, _prizeIndex, _pickAttempt))),
132+
uint256(keccak256(abi.encode(_entropy, _pickAttempt))),
121133
_numTokens
122134
);
123135
}
@@ -131,8 +143,8 @@ contract NftChanceBoosterHook is IPrizeHooks {
131143
_recipientTwab = twabController.getTwabBetween(boostedVault, _ownerOfToken, _tierStartTime, _tierEndTime);
132144
}
133145
if (_recipientTwab >= minTwabOverPrizePeriod) {
134-
// The owner of the selected NFT wins the prize!
135-
return (_ownerOfToken, abi.encode(_pickAttempt));
146+
// The owner of the selected NFT will be awarded the prize in the `afterClaimPrize` callback.
147+
return (address(this), abi.encode(_ownerOfToken));
136148
}
137149
}
138150
}
@@ -143,12 +155,19 @@ contract NftChanceBoosterHook is IPrizeHooks {
143155

144156
/// @inheritdoc IPrizeHooks
145157
/// @dev If the recipient is set to the prize pool, the prize will be contributed on behalf of the vault
146-
/// that is being boosted. Otherwise, it will do nothing (the prize will have already been sent to the
147-
/// randomly selected NFT winner).
148-
function afterClaimPrize(address, uint8, uint32, uint256 _prizeAmount, address _prizeRecipient, bytes memory) external {
149-
if (_prizeRecipient == address(prizePool) && _prizeAmount > 0) {
150-
prizePool.contributePrizeTokens(boostedVault, _prizeAmount);
151-
emit BoostedVaultWithPrize(address(prizePool), boostedVault, _prizeAmount);
158+
/// that is being boosted. Otherwise if this contract received the prize, it will transfer it to the
159+
/// winner passed in through the extra hook data.
160+
function afterClaimPrize(address _winner, uint8 _tier, uint32 _prizeIndex, uint256 _prizeAmount, address _prizeRecipient, bytes memory _data) external {
161+
if (_prizeAmount > 0) {
162+
if (_prizeRecipient == address(prizePool)) {
163+
uint256 _pickAttempts = abi.decode(_data, (uint256));
164+
prizePool.contributePrizeTokens(boostedVault, _prizeAmount);
165+
emit BoostedVaultWithPrize(address(prizePool), boostedVault, _prizeAmount, _pickAttempts);
166+
} else if (_prizeRecipient == address(this)) {
167+
address _nftWinner = abi.decode(_data, (address));
168+
prizePool.prizeToken().safeTransfer(_nftWinner, _prizeAmount);
169+
emit PrizeWonByNftHolder(_nftWinner, msg.sender, _winner, _tier, _prizeIndex, _prizeAmount);
170+
}
152171
}
153172
}
154173
}

test/prize-hooks/examples/nft-chance-booster/NftChanceBoosterHook.t.sol

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { ERC721ConsecutiveMock } from "openzeppelin-v5/mocks/token/ERC721Consecu
1111

1212
contract NftChanceBoosterHookTest is Test {
1313

14+
event BoostedVaultWithPrize(address indexed prizePool, address indexed boostedVault, uint256 prizeAmount, uint256 pickAttempts);
15+
event PrizeWonByNftHolder(address indexed nftWinner, address indexed vault, address indexed donor, uint8 tier, uint32 prizeIndex, uint256 prizeAmount);
16+
1417
uint256 fork;
1518
uint256 forkBlock = 17582112;
1619
uint256 forkTimestamp = 1721996771;
@@ -54,8 +57,11 @@ contract NftChanceBoosterHookTest is Test {
5457
// check a bunch of numbers and ensure there are no valid winners selected
5558
for (uint256 i = 0; i < 1000; i++) {
5659
vm.mockCall(address(prizePool), abi.encodeWithSelector(PrizePool.getWinningRandomNumber.selector), abi.encode(randomNumber + i));
57-
address recipient = callBeforeClaimPrize(0, 0);
60+
(address recipient, bytes memory hookData) = callBeforeClaimPrize(0, 0);
5861
assertEq(recipient, address(prizePool));
62+
(uint256 pickAttempts) = abi.decode(hookData, (uint256));
63+
assertGt(pickAttempts, 0);
64+
assertLt(pickAttempts, 10); // probably won't ever exceed 10 pick attempts, but this is not strictly required
5965
}
6066
}
6167

@@ -73,9 +79,12 @@ contract NftChanceBoosterHookTest is Test {
7379
uint256 numAliceWins;
7480
for (uint256 i = 0; i < 1000; i++) {
7581
vm.mockCall(address(prizePool), abi.encodeWithSelector(PrizePool.getWinningRandomNumber.selector), abi.encode(randomNumber + i));
76-
address recipient = callBeforeClaimPrize(0, 0);
77-
if (recipient == bob) numBobWins++;
78-
else if (recipient == alice) numAliceWins++;
82+
(address recipient, bytes memory hookData) = callBeforeClaimPrize(0, 0);
83+
if (recipient == address(nftBooster)) {
84+
(address winner) = abi.decode(hookData, (address));
85+
if (winner == bob) numBobWins++;
86+
else if (winner == alice) numAliceWins++;
87+
}
7988
}
8089
assertGt(numBobWins, 800);
8190
assertGt(numAliceWins, 50);
@@ -96,9 +105,12 @@ contract NftChanceBoosterHookTest is Test {
96105
uint256 numAliceWins;
97106
for (uint256 i = 0; i < 1000; i++) {
98107
vm.mockCall(address(prizePool), abi.encodeWithSelector(PrizePool.getWinningRandomNumber.selector), abi.encode(randomNumber + i));
99-
address recipient = callBeforeClaimPrize(0, 0);
100-
if (recipient == bob) numBobWins++;
101-
else if (recipient == alice) numAliceWins++;
108+
(address recipient, bytes memory hookData) = callBeforeClaimPrize(0, 0);
109+
if (recipient == address(nftBooster)) {
110+
(address winner) = abi.decode(hookData, (address));
111+
if (winner == bob) numBobWins++;
112+
else if (winner == alice) numAliceWins++;
113+
}
102114
}
103115
assertGt(numBobWins, 900);
104116
assertEq(numAliceWins, 0);
@@ -111,22 +123,46 @@ contract NftChanceBoosterHookTest is Test {
111123
// check a bunch of numbers and ensure there are no valid winners selected
112124
for (uint256 i = 0; i < 1000; i++) {
113125
vm.mockCall(address(prizePool), abi.encodeWithSelector(PrizePool.getWinningRandomNumber.selector), abi.encode(randomNumber + i));
114-
address recipient = callBeforeClaimPrize(0, 0);
126+
(address recipient, bytes memory hookData) = callBeforeClaimPrize(0, 0);
115127
assertEq(recipient, address(prizePool));
128+
(uint256 pickAttempts) = abi.decode(hookData, (uint256));
129+
assertGt(pickAttempts, 0);
130+
assertLt(pickAttempts, 10); // probably won't ever exceed 10 pick attempts, but this is not strictly required
116131
}
117132
}
118133

119-
function callBeforeClaimPrize(uint8 tier, uint32 prizeIndex) internal returns (address) {
134+
function testAfterClaimPrizeRedirectsPrize() public {
135+
address alice = holders[1];
136+
deal(address(prizePool.prizeToken()), address(nftBooster), 1e18);
137+
assertEq(prizePool.prizeToken().balanceOf(alice), 0);
138+
assertEq(prizePool.prizeToken().balanceOf(address(nftBooster)), 1e18);
139+
vm.expectEmit();
140+
emit PrizeWonByNftHolder(alice, address(this), address(1), 2, 3, 1e18);
141+
(bool success,) = address(nftBooster).call{ gas: 150_000 }(abi.encodeWithSelector(IPrizeHooks.afterClaimPrize.selector, address(1), 2, 3, 1e18, address(nftBooster), abi.encode(address(alice))));
142+
require(success, "afterClaimPrize failed");
143+
assertEq(prizePool.prizeToken().balanceOf(alice), 1e18);
144+
assertEq(prizePool.prizeToken().balanceOf(address(nftBooster)), 0);
145+
}
146+
147+
function testAfterClaimPrizeContributesPrize() public {
148+
deal(address(prizePool.prizeToken()), address(prizePool), 1e18 + prizePool.prizeToken().balanceOf(address(prizePool)));
149+
vm.expectEmit();
150+
emit BoostedVaultWithPrize(address(prizePool), address(this), 1e18, 5);
151+
(bool success,) = address(nftBooster).call{ gas: 150_000 }(abi.encodeWithSelector(IPrizeHooks.afterClaimPrize.selector, address(1), 2, 3, 1e18, address(prizePool), abi.encode(uint256(5))));
152+
require(success, "afterClaimPrize failed");
153+
assertEq(prizePool.getContributedBetween(address(this), prizePool.getOpenDrawId(), prizePool.getOpenDrawId()), 1e18);
154+
}
155+
156+
function callBeforeClaimPrize(uint8 tier, uint32 prizeIndex) internal returns (address recipient, bytes memory hookData) {
120157
(bool success, bytes memory data) = address(nftBooster).call{ gas: 150_000 }(abi.encodeWithSelector(IPrizeHooks.beforeClaimPrize.selector, address(0), tier, prizeIndex, 0, address(0)));
121158
require(success, "beforeClaimPrize failed");
122-
(address recipient, bytes memory hookData) = abi.decode(data, (address,bytes));
159+
(recipient, hookData) = abi.decode(data, (address,bytes));
123160
// if (hookData.length > 0) {
124161
// uint256 pickAttempt = abi.decode(hookData, (uint256));
125162
// if (pickAttempt > 0) {
126163
// console2.log("pick attempt", pickAttempt);
127164
// }
128165
// }
129-
return recipient;
130166
}
131167

132168
}

0 commit comments

Comments
 (0)