Skip to content

Commit

Permalink
Add initial design for WorldIdVerifiedPrizeVault
Browse files Browse the repository at this point in the history
  • Loading branch information
trmid committed Oct 2, 2024
1 parent 1ffe044 commit 7d4e3a3
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 64 deletions.
16 changes: 0 additions & 16 deletions script/Foo.s.sol

This file was deleted.

8 changes: 0 additions & 8 deletions src/Foo.sol

This file was deleted.

357 changes: 357 additions & 0 deletions src/WorldIdVerifiedPrizeVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { TwabERC20, TwabController, ERC20 } from "../lib/pt-v5-vault/src/TwabERC20.sol";
import { PrizePool } from "../lib/pt-v5-vault/lib/pt-v5-prize-pool/src/PrizePool.sol";
import { Ownable } from "../lib/pt-v5-vault/lib/owner-manager-contracts/contracts/Ownable.sol";
import { IClaimable } from "../lib/pt-v5-vault/lib/pt-v5-claimable-interface/src/interfaces/IClaimable.sol";
import { IERC4626 } from "../lib/pt-v5-vault/lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
import { IERC20, SafeERC20 } from "../lib/pt-v5-vault/lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import { IWorldIdAddressBook } from "./interfaces/IWorldIdAddressBook.sol";

contract WorldIdVerifiedPrizeVault is TwabERC20, Ownable, IERC4626, IClaimable {
using SafeERC20 for IERC20;

////////////////////////////////////////////////////////////////////////////////
// Public Constants and Variables
////////////////////////////////////////////////////////////////////////////////

/// @notice Address of the claimer
address public claimer;

/// @notice Address that will receive any excess prize value due to an account's
/// TWAB exceeding the `accountDepositLimit` at the time of winning a prize.
address public prizeExcessRecipient;

/// @notice The account deposit limit in assets
uint256 public accountDepositLimit;

/// @notice The prize pool this vault is participating in
PrizePool public immutable prizePool;

/// @notice World ID address book
IWorldIdAddressBook public immutable worldIdAddressBook;

/// @notice Address of the underlying deposit asset
address public immutable asset;

////////////////////////////////////////////////////////////////////////////////
// Events
////////////////////////////////////////////////////////////////////////////////

/// @notice Emitted when the claimer is set
/// @param claimer The new claimer address
event SetClaimer(address indexed claimer);

/// @notice Emitted when the prize excess recipient is set
/// @param prizeExcessRecipient The new prize excess recipient
event SetPrizeExcessRecipient(address indexed prizeExcessRecipient);

/// @notice Emitted when the account deposit limit is set
/// @param accountDepositLimit The new account deposit limit
event SetAccountDepositLimit(uint256 accountDepositLimit);

////////////////////////////////////////////////////////////////////////////////
// Errors
////////////////////////////////////////////////////////////////////////////////

/// @notice Thrown if the caller of `claimPrize` is not the `claimer`
/// @param caller The caller of the function
/// @param claimer The permitted claimer
error CallerNotClaimer(address caller, address claimer);

/// @notice Thrown when an account requires World ID verification
/// @param account The account that is not verified
error AccountNotVerifiedWithWorldId(address account);

/// @notice Thrown when a deposit exceeds the account deposit limit
/// @param account The account receiving the deposit
/// @param currentBalance The current account balance
/// @param newBalance The new balance addition
/// @param accountDepositLimit The account deposit limit
error DepositLimitExceeded(
address account,
uint256 currentBalance,
uint256 newBalance,
uint256 accountDepositLimit
);

/// @notice Thrown when a deposit is made with zero assets
error DepositZeroAssets();

/// @notice Thrown when a withdrawal is made with zero assets
error WithdrawZeroAssets();

////////////////////////////////////////////////////////////////////////////////
// Modifiers
////////////////////////////////////////////////////////////////////////////////

/// @notice Throws if the account is not currently verified with a world ID.
/// @param _account The account to check
modifier onlyVerifiedWorldId(address _account) {
if (worldIdAddressBook.addressVerifiedUntil(_account) < block.timestamp) {
revert AccountNotVerifiedWithWorldId(_account);
}
_;
}

////////////////////////////////////////////////////////////////////////////////
// Constructor
////////////////////////////////////////////////////////////////////////////////

/// @notice Constructor
/// @param name_ The prize vault name
/// @param symbol_ The prize vault symbol
/// @param prizePool_ The prize pool this vault is participating in
/// @param worldIdAddressBook_ The world ID address book to use for verifying addresses
/// @param claimer_ The initial claimer for the prize vault
/// @param owner_ The initial owner for the prize vault
/// @param prizeExcessRecipient_ The initial prize excess recipient
/// @param accountDepositLimit_ The initial account deposit limit in assets
constructor(
string memory name_,
string memory symbol_,
PrizePool prizePool_,
IWorldIdAddressBook worldIdAddressBook_,
address claimer_,
address owner_,
address prizeExcessRecipient_,
uint256 accountDepositLimit_
) TwabERC20(name_, symbol_, prizePool_.twabController()) Ownable(owner_) {
assert(address(worldIdAddressBook_) != address(0));
prizePool = prizePool_;
asset = address(prizePool_.prizeToken());
worldIdAddressBook = worldIdAddressBook_;
_setClaimer(claimer_);
_setPrizeExcessRecipient(prizeExcessRecipient_);
_setAccountDepositLimit(accountDepositLimit_);
}

////////////////////////////////////////////////////////////////////////////////
// Owner Functions
////////////////////////////////////////////////////////////////////////////////

/// @notice Sets a new claimer
/// @dev Only Owner
/// @param _claimer The new claimer to set
function setClaimer(address _claimer) external onlyOwner {
_setClaimer(_claimer);
}

/// @notice Sets a new prize excess recipient
/// @dev Only Owner
/// @param _prizeExcessRecipient The new prize excess recipient to set
function setPrizeExcessRecipient(address _prizeExcessRecipient) external onlyOwner {
_setPrizeExcessRecipient(_prizeExcessRecipient);
}

/// @notice Sets a new account deposit limit
/// @dev Only Owner
/// @param _accountDepositLimit The new account deposit limit to set
function setAccountDepositLimit(uint256 _accountDepositLimit) external onlyOwner {
_setAccountDepositLimit(_accountDepositLimit);
}

////////////////////////////////////////////////////////////////////////////////
// IClaimable Implementation
////////////////////////////////////////////////////////////////////////////////

/// @inheritdoc IClaimable
function claimPrize(
address _winner,
uint8 _tier,
uint32 _prizeIndex,
uint96 _claimReward,
address _claimRewardRecipient
) external onlyVerifiedWorldId(_winner) returns (uint256) {
if (msg.sender != claimer) revert CallerNotClaimer(msg.sender, claimer);
uint24 _lastAwardedDrawId = prizePool.getLastAwardedDrawId();
uint256 _startTimestamp = prizePool.drawOpensAt(
prizePool.computeRangeStartDrawIdInclusive(
_lastAwardedDrawId,
prizePool.getTierAccrualDurationInDraws(_tier)
)
);
uint256 _endTimestamp = prizePool.drawClosesAt(_lastAwardedDrawId);
uint256 _winnerTwabForPrizeTier = twabController.getTwabBetween(address(this), _winner, _startTimestamp, _endTimestamp);
uint256 _totalPrizeValue = prizePool.claimPrize(
_winner,
_tier,
_prizeIndex,
address(this),
_claimReward,
_claimRewardRecipient
);
uint256 _prizeAmountWon = _totalPrizeValue - _claimReward;
if (_winnerTwabForPrizeTier > accountDepositLimit) {
// Limit the prize amount won proportionally based on how much the winner's TWAB exceeds the deposit limit
_prizeAmountWon = (_prizeAmountWon * accountDepositLimit) / _winnerTwabForPrizeTier;

// Send the excess to the prize excess recipient
IERC20(asset).safeTransfer(prizeExcessRecipient, _totalPrizeValue - _prizeAmountWon - _claimReward);
}
if (_prizeAmountWon > 0) {
// Mint shares up to the winner's deposit limit and transfer the rest as assets if any
uint256 _winnerDepositLimit = maxDeposit(_winner);
if (_prizeAmountWon > _winnerDepositLimit) {
_mint(_winner, _winnerDepositLimit);
IERC20(asset).safeTransfer(_winner, _prizeAmountWon - _winnerDepositLimit);
} else {
_mint(_winner, _prizeAmountWon);
}
}
return _totalPrizeValue;
}

////////////////////////////////////////////////////////////////////////////////
// IERC20 Overrides
////////////////////////////////////////////////////////////////////////////////

/// @inheritdoc ERC20
/// @dev Prevents share token transfers if they would exceed the deposit limit of the recipient
function _beforeTokenTransfer(address /*_from*/, address _to, uint256 _amount) internal virtual override onlyVerifiedWorldId(_to) {
if (_amount > maxDeposit(_to)) {
revert DepositLimitExceeded(_to, balanceOf(_to), _amount, accountDepositLimit);
}
}

////////////////////////////////////////////////////////////////////////////////
// IERC4626 Implementation
////////////////////////////////////////////////////////////////////////////////

/// @inheritdoc IERC4626
function totalAssets() external view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}

/// @inheritdoc IERC4626
function convertToShares(uint256 _assets) external view returns (uint256) {
return _assets;
}

/// @inheritdoc IERC4626
function convertToAssets(uint256 _shares) external view returns (uint256) {
return _shares;
}

/// @inheritdoc IERC4626
/// @dev limited by the per-account limiter and world ID verification
function maxDeposit(address _receiver) public view returns (uint256) {
if (!_isAccountVerified(_receiver)) {
return 0;
} else {
uint256 _receiverBalance = balanceOf(_receiver);
return _receiverBalance >= accountDepositLimit ? 0 : accountDepositLimit - _receiverBalance;
}
}

/// @inheritdoc IERC4626
function previewDeposit(uint256 _assets) external view returns (uint256) {
return _assets;
}

/// @inheritdoc IERC4626
function deposit(uint256 _assets, address _receiver) public returns (uint256) {
if (_assets == 0) {
revert DepositZeroAssets();
}
if (_assets > maxDeposit(_receiver)) {
revert DepositLimitExceeded(_receiver, balanceOf(_receiver), _assets, accountDepositLimit);
}
IERC20(asset).safeTransferFrom(msg.sender, address(this), _assets);
_mint(_receiver, _assets);
emit IERC4626.Deposit(msg.sender, _receiver, _assets, _assets);
return _assets;
}

/// @inheritdoc IERC4626
/// @dev limited by the per-account limiter and world ID verification
function maxMint(address _receiver) external view returns (uint256) {
return maxDeposit(_receiver);
}

/// @inheritdoc IERC4626
function previewMint(uint256 _shares) external view returns (uint256) {
return _shares;
}

/// @inheritdoc IERC4626
function mint(uint256 _shares, address _receiver) external returns (uint256) {
return deposit(_shares, _receiver);
}

/// @inheritdoc IERC4626
function maxWithdraw(address _owner) external view returns (uint256) {
return balanceOf(_owner);
}

/// @inheritdoc IERC4626
function previewWithdraw(uint256 _assets) external view returns (uint256) {
return _assets;
}

/// @inheritdoc IERC4626
function withdraw(uint256 _assets, address _receiver, address _owner) public returns (uint256) {
if (_assets == 0) {
revert WithdrawZeroAssets();
}
if (msg.sender != _owner) {
_spendAllowance(_owner, msg.sender, _assets);
}
_burn(_owner, _assets);
IERC20(asset).safeTransfer(_receiver, _assets);
emit Withdraw(msg.sender, _receiver, _owner, _assets, _assets);
return _assets;
}

/// @inheritdoc IERC4626
function maxRedeem(address _owner) external view returns (uint256) {
return balanceOf(_owner);
}

/// @inheritdoc IERC4626
function previewRedeem(uint256 _shares) external view returns (uint256) {
return _shares;
}

/// @inheritdoc IERC4626
function redeem(uint256 _shares, address _receiver, address _owner) external returns (uint256) {
return withdraw(_shares, _receiver, _owner);
}

////////////////////////////////////////////////////////////////////////////////
// Internal Functions
////////////////////////////////////////////////////////////////////////////////

/// @notice Sets the claimer address
/// @dev Will revert if `_claimer` is address zero
/// @param _claimer The new claimer address
function _setClaimer(address _claimer) internal {
assert(_claimer != address(0));
claimer = _claimer;
emit SetClaimer(_claimer);
}

/// @notice Sets the prize excess recipient
/// @dev Will revert if `_prizeExcessRecipient` is address zero
/// @param _prizeExcessRecipient The new prize excess recipient address
function _setPrizeExcessRecipient(address _prizeExcessRecipient) internal {
assert(_prizeExcessRecipient != address(0));
prizeExcessRecipient = _prizeExcessRecipient;
emit SetPrizeExcessRecipient(_prizeExcessRecipient);
}

/// @notice Sets a new account deposit limit
/// @param _accountDepositLimit The new account deposit limit
function _setAccountDepositLimit(uint256 _accountDepositLimit) internal {
accountDepositLimit = _accountDepositLimit;
emit SetAccountDepositLimit(_accountDepositLimit);
}

/// @notice Returns true if the account is actively verified with the World ID address book
/// @param _account The account to check
function _isAccountVerified(address _account) internal view returns (bool) {
return worldIdAddressBook.addressVerifiedUntil(_account) >= block.timestamp;
}

}
6 changes: 6 additions & 0 deletions src/interfaces/IWorldIdAddressBook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IWorldIdAddressBook {
function addressVerifiedUntil(address account) external view returns (uint256);
}
Loading

0 comments on commit 7d4e3a3

Please sign in to comment.