diff --git a/script/Foo.s.sol b/script/Foo.s.sol deleted file mode 100644 index 47d35ed..0000000 --- a/script/Foo.s.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import { Script } from "forge-std/Script.sol"; -import { Foo } from "../src/Foo.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract FooScript is Script { - Foo internal foo; - - function run() public { - vm.startBroadcast(); - foo = new Foo(); - vm.stopBroadcast(); - } -} diff --git a/src/Foo.sol b/src/Foo.sol deleted file mode 100644 index 1bf1320..0000000 --- a/src/Foo.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -contract Foo { - function getFoo() external pure returns (string memory) { - return "Foo"; - } -} diff --git a/src/WorldIdVerifiedPrizeVault.sol b/src/WorldIdVerifiedPrizeVault.sol new file mode 100644 index 0000000..0b06fc9 --- /dev/null +++ b/src/WorldIdVerifiedPrizeVault.sol @@ -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; + } + +} diff --git a/src/interfaces/IWorldIdAddressBook.sol b/src/interfaces/IWorldIdAddressBook.sol new file mode 100644 index 0000000..6f2ed60 --- /dev/null +++ b/src/interfaces/IWorldIdAddressBook.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IWorldIdAddressBook { + function addressVerifiedUntil(address account) external view returns (uint256); +} \ No newline at end of file diff --git a/test/Foo.t.sol b/test/Foo.t.sol deleted file mode 100644 index 101faee..0000000 --- a/test/Foo.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { Foo } from "../src/Foo.sol"; - -interface IERC20 { - function balanceOf(address account) external view returns (uint256); -} - -/// @dev See the "Writing Tests" section in the Foundry Book if this is your first time with Forge. -/// https://book.getfoundry.sh/forge/writing-tests -contract FooTest is Test { - uint256 public mainnetFork; - - Foo public fooContract = new Foo(); - - function setUp() public { - mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 16_428_000); - } - - /// @dev Simple test. Run Forge with `-vvvv` to see stack traces. - function test() external { - string memory foo = fooContract.getFoo(); - - assertEq(foo, "Foo"); - } - - /// @dev Test that runs against a fork of Ethereum Mainnet. You need to set `MAINNET_RPC_URL` in your `.envrc` - function testFork() external { - vm.selectFork(mainnetFork); - - address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address holder = 0x7713974908Be4BEd47172370115e8b1219F4A5f0; - uint256 actualBalance = IERC20(usdc).balanceOf(holder); - uint256 expectedBalance = 196_307_713.810457e6; - assertEq(actualBalance, expectedBalance); - } -}