diff --git a/contracts/foundry.toml b/contracts/foundry.toml index c8c2f576..4a0840fe 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -17,7 +17,8 @@ remappings = [ "test/=test", "@openzeppelin/=node_modules/@openzeppelin/", "@openzeppelin-upgrades/contracts/=node_modules/@openzeppelin/contracts-upgradeable", - "erc6551/=node_modules/erc6551/" + "erc6551/=node_modules/erc6551/", + "solady/=node_modules/solady/", ] fs_permissions = [ diff --git a/contracts/package.json b/contracts/package.json index 83bfed06..165e49cb 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -34,6 +34,7 @@ "@openzeppelin/contracts": "5.0.2", "@openzeppelin/contracts-upgradeable": "5.0.2", "erc6551": "^0.3.1", + "solady": "^0.0.259", "solmate": "^6.2.0" } } diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 53f97b57..59c805c3 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: erc6551: specifier: ^0.3.1 version: 0.3.1 + solady: + specifier: ^0.0.259 + version: 0.0.259 solmate: specifier: ^6.2.0 version: 6.2.0 @@ -648,6 +651,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + solady@0.0.259: + resolution: {integrity: sha512-GA23TidJxs11hdsjnh6CMi7pIRStvNerJK+t80F4VwXY3JGjxn+i3W5/mpQZAyC883gwgxLe+6Pauiemd46ezA==} + solhint-plugin-prettier@0.1.0: resolution: {integrity: sha512-SDOTSM6tZxZ6hamrzl3GUgzF77FM6jZplgL2plFBclj/OjKP8Z3eIPojKU73gRr0MvOS8ACZILn8a5g0VTz/Gw==} peerDependencies: @@ -1357,6 +1363,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + solady@0.0.259: {} + solhint-plugin-prettier@0.1.0(prettier-plugin-solidity@1.4.1(prettier@3.3.3))(prettier@3.3.3): dependencies: '@prettier/sync': 0.3.0(prettier@3.3.3) diff --git a/contracts/script/GenerateAlloc.s.sol b/contracts/script/GenerateAlloc.s.sol index 55589b5e..427de87d 100644 --- a/contracts/script/GenerateAlloc.s.sol +++ b/contracts/script/GenerateAlloc.s.sol @@ -17,6 +17,7 @@ import { InitializableHelper } from "./utils/InitializableHelper.sol"; import { Predeploys } from "../src/libraries/Predeploys.sol"; import { Create3 } from "../src/deploy/Create3.sol"; import { ERC6551Registry } from "erc6551/ERC6551Registry.sol"; +import { WIP } from "../src/token/WIP.sol"; /** * @title GenerateAlloc * @dev A script to generate the alloc section of EL genesis @@ -185,6 +186,7 @@ contract GenerateAlloc is Script { setCreate3(); deployTimelock(); setERC6551(); + setWIP(); // Set proxies for all predeploys setProxies(); @@ -368,6 +370,19 @@ contract GenerateAlloc is Script { console2.log("ERC6551 deployed at:", Predeploys.ERC6551Registry); } + function setWIP() internal { + address tmp = address(new WIP()); + vm.etch(Predeploys.WIP, tmp.code); + + // reset tmp + vm.etch(tmp, ""); + vm.store(tmp, 0, "0x"); + vm.resetNonce(tmp); + + vm.deal(Predeploys.WIP, 1); + console2.log("WIP deployed at:", Predeploys.WIP); + } + function setAllocations() internal { // EL Predeploys // Geth precompile 1 wei allocation (Accounts with 0 balance and no EVM code may be removed from diff --git a/contracts/src/libraries/Predeploys.sol b/contracts/src/libraries/Predeploys.sol index 575f95dd..75adba34 100644 --- a/contracts/src/libraries/Predeploys.sol +++ b/contracts/src/libraries/Predeploys.sol @@ -10,7 +10,7 @@ library Predeploys { uint256 internal constant NamespaceSize = 1024; /// @notice Predeploys - address internal constant WIP = 0x1513000000000000000000000000000000000000; + address internal constant WIP = 0x1516000000000000000000000000000000000000; address internal constant Staking = 0xCCcCcC0000000000000000000000000000000001; address internal constant UBIPool = 0xCccCCC0000000000000000000000000000000002; address internal constant Upgrades = 0xccCCcc0000000000000000000000000000000003; diff --git a/contracts/src/token/WIP.sol b/contracts/src/token/WIP.sol new file mode 100644 index 00000000..9cebd47e --- /dev/null +++ b/contracts/src/token/WIP.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.23; + +import { ERC20 } from "solady/src/tokens/ERC20.sol"; +/// @notice Wrapped IP implementation. +/// @author Inspired by WETH9 (https://github.com/dapphub/ds-weth/blob/master/src/weth9.sol) +contract WIP is ERC20 { + /// @notice emitted when IP is deposited in exchange for WIP + event Deposit(address indexed from, uint amount); + /// @notice emitted when WIP is withdrawn in exchange for IP + event Withdrawal(address indexed to, uint amount); + /// @notice emitted when a transfer of IP fails + error IPTransferFailed(); + + /// @notice triggered when IP is deposited in exchange for WIP + receive() external payable { + deposit(); + } + + /// @notice deposits IP in exchange for WIP + /// @dev the amount of IP deposited is equal to the amount of WIP minted + function deposit() public payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + /// @notice withdraws WIP in exchange for IP + /// @dev the amount of IP minted is equal to the amount of WIP burned + /// @param value the amount of WIP to burn and withdraw + function withdraw(uint value) external { + _burn(msg.sender, value); + (bool success, ) = msg.sender.call{ value: value }(""); + if (!success) { + revert IPTransferFailed(); + } + emit Withdrawal(msg.sender, value); + } + + /// @notice returns the name of the token + function name() public view override returns (string memory) { + return "Wrapped IP"; + } + + /// @notice returns the symbol of the token + function symbol() public view override returns (string memory) { + return "WIP"; + } + + /// @dev Sets Permit2 contract's allowance to infinity. + function _givePermit2InfiniteAllowance() internal pure override returns (bool) { + return true; + } +} diff --git a/contracts/test/token/WIP.t.sol b/contracts/test/token/WIP.t.sol new file mode 100644 index 00000000..a352df85 --- /dev/null +++ b/contracts/test/token/WIP.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Test } from "../utils/Test.sol"; +import { WIP } from "../../src/token/WIP.sol"; + +contract ContractWithoutReceive {} + +contract WIPTest is Test { + function testMetadata() public view { + assertEq(wip.name(), "Wrapped IP"); + assertEq(wip.symbol(), "WIP"); + assertEq(wip.decimals(), 18); + } + + function testFallbackDeposit() public { + assertEq(wip.balanceOf(address(this)), 0); + assertEq(wip.totalSupply(), 0); + + (bool success, ) = address(wip).call{ value: 1 ether }(""); + assertTrue(success); + + assertEq(wip.balanceOf(address(this)), 1 ether); + assertEq(wip.totalSupply(), 1 ether); + } + + function testDeposit() public { + assertEq(wip.balanceOf(address(this)), 0); + assertEq(wip.totalSupply(), 0); + + wip.deposit{ value: 1 ether }(); + + assertEq(wip.balanceOf(address(this)), 1 ether); + assertEq(wip.totalSupply(), 1 ether); + } + + function testWithdraw() public { + uint256 startingBalance = address(this).balance; + + wip.deposit{ value: 1 ether }(); + + wip.withdraw(1 ether); + + uint256 balanceAfterWithdraw = address(this).balance; + + assertEq(balanceAfterWithdraw, startingBalance); + assertEq(wip.balanceOf(address(this)), 0); + assertEq(wip.totalSupply(), 0); + } + + function testPartialWithdraw() public { + wip.deposit{ value: 1 ether }(); + + uint256 balanceBeforeWithdraw = address(this).balance; + + wip.withdraw(0.5 ether); + + uint256 balanceAfterWithdraw = address(this).balance; + + assertEq(balanceAfterWithdraw, balanceBeforeWithdraw + 0.5 ether); + assertEq(wip.balanceOf(address(this)), 0.5 ether); + assertEq(wip.totalSupply(), 0.5 ether); + } + + function testWithdrawToContractWithoutReceiveReverts() public { + address owner = address(new ContractWithoutReceive()); + + vm.deal(owner, 1 ether); + + vm.prank(owner); + wip.deposit{ value: 1 ether }(); + + assertEq(wip.balanceOf(owner), 1 ether); + + vm.expectRevert(WIP.IPTransferFailed.selector); + vm.prank(owner); + wip.withdraw(1 ether); + } + + function testFallbackDeposit(uint256 amount) public { + amount = _bound(amount, 0, address(this).balance); + + assertEq(wip.balanceOf(address(this)), 0); + assertEq(wip.totalSupply(), 0); + + (bool success, ) = address(wip).call{ value: amount }(""); + assertTrue(success); + + assertEq(wip.balanceOf(address(this)), amount); + assertEq(wip.totalSupply(), amount); + } + + function testDeposit(uint256 amount) public { + amount = _bound(amount, 0, address(this).balance); + + assertEq(wip.balanceOf(address(this)), 0); + assertEq(wip.totalSupply(), 0); + + wip.deposit{ value: amount }(); + + assertEq(wip.balanceOf(address(this)), amount); + assertEq(wip.totalSupply(), amount); + } + + function testWithdraw(uint256 depositAmount, uint256 withdrawAmount) public { + depositAmount = _bound(depositAmount, 0, address(this).balance); + withdrawAmount = _bound(withdrawAmount, 0, depositAmount); + + wip.deposit{ value: depositAmount }(); + + uint256 balanceBeforeWithdraw = address(this).balance; + + wip.withdraw(withdrawAmount); + + uint256 balanceAfterWithdraw = address(this).balance; + + assertEq(balanceAfterWithdraw, balanceBeforeWithdraw + withdrawAmount); + assertEq(wip.balanceOf(address(this)), depositAmount - withdrawAmount); + assertEq(wip.totalSupply(), depositAmount - withdrawAmount); + } + + receive() external payable {} +} diff --git a/contracts/test/utils/Test.sol b/contracts/test/utils/Test.sol index 4f06ea72..8f50641d 100644 --- a/contracts/test/utils/Test.sol +++ b/contracts/test/utils/Test.sol @@ -13,6 +13,7 @@ import { Create3 } from "../../src/deploy/Create3.sol"; import { GenerateAlloc } from "../../script/GenerateAlloc.s.sol"; import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol"; import { ERC6551Registry } from "erc6551/ERC6551Registry.sol"; +import { WIP } from "../../src/token/WIP.sol"; contract Test is ForgeTest { address internal admin = address(0x123); @@ -27,6 +28,7 @@ contract Test is ForgeTest { Create3 internal create3; ERC6551Registry internal erc6551Registry; TimelockController internal timelock; + WIP internal wip; function setUp() public virtual { GenerateAlloc initializer = new GenerateAlloc(); @@ -37,6 +39,7 @@ contract Test is ForgeTest { upgradeEntrypoint = UpgradeEntrypoint(Predeploys.Upgrades); ubiPool = UBIPool(Predeploys.UBIPool); create3 = Create3(Predeploys.Create3); + wip = WIP(payable(Predeploys.WIP)); erc6551Registry = ERC6551Registry(Predeploys.ERC6551Registry); address timelockAddress = create3.getDeployed(deployer, keccak256("STORY_TIMELOCK_CONTROLLER")); timelock = TimelockController(payable(timelockAddress));