From a90e40ee0308b9f055c5e3ff4d61d7fd896d1c54 Mon Sep 17 00:00:00 2001 From: Kingster Date: Tue, 22 Oct 2024 20:06:15 -0700 Subject: [PATCH 1/3] Introduce Wrapped IP and add to predeploy --- contracts/foundry.toml | 3 +- contracts/package.json | 1 + contracts/pnpm-lock.yaml | 8 ++ contracts/script/GenerateAlloc.s.sol | 15 +++ contracts/src/libraries/Predeploys.sol | 2 +- contracts/src/token/WIP.sol | 53 +++++++++++ contracts/test/token/WIP.t.sol | 124 +++++++++++++++++++++++++ contracts/test/utils/Test.sol | 3 + 8 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 contracts/src/token/WIP.sol create mode 100644 contracts/test/token/WIP.t.sol 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 d0fe143c..a876f165 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 @@ -177,6 +178,7 @@ contract GenerateAlloc is Script { setCreate3(); deployTimelock(); setERC6551(); + setWIP(); // predeploys that are upgradable setProxy(Predeploys.Staking); @@ -347,6 +349,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 7e610506..f6653acd 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..8c99e4d5 --- /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; + } +} \ No newline at end of file diff --git a/contracts/test/token/WIP.t.sol b/contracts/test/token/WIP.t.sol new file mode 100644 index 00000000..1607077d --- /dev/null +++ b/contracts/test/token/WIP.t.sol @@ -0,0 +1,124 @@ +// 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 9bc7a1a2..4627d322 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)); From 7df245a5399650fa273333852d1661181bf0bba9 Mon Sep 17 00:00:00 2001 From: Kingster Date: Tue, 22 Oct 2024 20:16:12 -0700 Subject: [PATCH 2/3] fix lint --- contracts/src/token/WIP.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/token/WIP.sol b/contracts/src/token/WIP.sol index 8c99e4d5..9cebd47e 100644 --- a/contracts/src/token/WIP.sol +++ b/contracts/src/token/WIP.sol @@ -6,9 +6,9 @@ import { ERC20 } from "solady/src/tokens/ERC20.sol"; /// @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); + event Deposit(address indexed from, uint amount); /// @notice emitted when WIP is withdrawn in exchange for IP - event Withdrawal(address indexed to, uint amount); + event Withdrawal(address indexed to, uint amount); /// @notice emitted when a transfer of IP fails error IPTransferFailed(); @@ -29,7 +29,7 @@ contract WIP is ERC20 { /// @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}(""); + (bool success, ) = msg.sender.call{ value: value }(""); if (!success) { revert IPTransferFailed(); } @@ -50,4 +50,4 @@ contract WIP is ERC20 { function _givePermit2InfiniteAllowance() internal pure override returns (bool) { return true; } -} \ No newline at end of file +} From 041c657dfdb271dd02acb8a1546a852a937ed013 Mon Sep 17 00:00:00 2001 From: Kingster Date: Tue, 22 Oct 2024 20:22:41 -0700 Subject: [PATCH 3/3] fix lint --- contracts/src/libraries/Predeploys.sol | 2 +- contracts/test/token/WIP.t.sol | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/src/libraries/Predeploys.sol b/contracts/src/libraries/Predeploys.sol index f6653acd..7e217f00 100644 --- a/contracts/src/libraries/Predeploys.sol +++ b/contracts/src/libraries/Predeploys.sol @@ -20,7 +20,7 @@ library Predeploys { address internal constant Create3 = 0x9fBB3DF7C40Da2e5A0dE984fFE2CCB7C47cd0ABf; /// @notice ERC6551Registry address -/// @dev The common address for the ERC6551Registry across all chains defined by ERC-6551 + /// @dev The common address for the ERC6551Registry across all chains defined by ERC-6551 address internal constant ERC6551Registry = 0x000000006551c19487814612e58FE06813775758; /// @notice Return true if `addr` is not proxied diff --git a/contracts/test/token/WIP.t.sol b/contracts/test/token/WIP.t.sol index 1607077d..a352df85 100644 --- a/contracts/test/token/WIP.t.sol +++ b/contracts/test/token/WIP.t.sol @@ -17,7 +17,7 @@ contract WIPTest is Test { assertEq(wip.balanceOf(address(this)), 0); assertEq(wip.totalSupply(), 0); - (bool success, ) = address(wip).call{value: 1 ether}(""); + (bool success, ) = address(wip).call{ value: 1 ether }(""); assertTrue(success); assertEq(wip.balanceOf(address(this)), 1 ether); @@ -28,7 +28,7 @@ contract WIPTest is Test { assertEq(wip.balanceOf(address(this)), 0); assertEq(wip.totalSupply(), 0); - wip.deposit{value: 1 ether}(); + wip.deposit{ value: 1 ether }(); assertEq(wip.balanceOf(address(this)), 1 ether); assertEq(wip.totalSupply(), 1 ether); @@ -37,7 +37,7 @@ contract WIPTest is Test { function testWithdraw() public { uint256 startingBalance = address(this).balance; - wip.deposit{value: 1 ether}(); + wip.deposit{ value: 1 ether }(); wip.withdraw(1 ether); @@ -49,7 +49,7 @@ contract WIPTest is Test { } function testPartialWithdraw() public { - wip.deposit{value: 1 ether}(); + wip.deposit{ value: 1 ether }(); uint256 balanceBeforeWithdraw = address(this).balance; @@ -68,7 +68,7 @@ contract WIPTest is Test { vm.deal(owner, 1 ether); vm.prank(owner); - wip.deposit{value: 1 ether}(); + wip.deposit{ value: 1 ether }(); assertEq(wip.balanceOf(owner), 1 ether); @@ -83,7 +83,7 @@ contract WIPTest is Test { assertEq(wip.balanceOf(address(this)), 0); assertEq(wip.totalSupply(), 0); - (bool success, ) = address(wip).call{value: amount}(""); + (bool success, ) = address(wip).call{ value: amount }(""); assertTrue(success); assertEq(wip.balanceOf(address(this)), amount); @@ -96,7 +96,7 @@ contract WIPTest is Test { assertEq(wip.balanceOf(address(this)), 0); assertEq(wip.totalSupply(), 0); - wip.deposit{value: amount}(); + wip.deposit{ value: amount }(); assertEq(wip.balanceOf(address(this)), amount); assertEq(wip.totalSupply(), amount); @@ -106,7 +106,7 @@ contract WIPTest is Test { depositAmount = _bound(depositAmount, 0, address(this).balance); withdrawAmount = _bound(withdrawAmount, 0, depositAmount); - wip.deposit{value: depositAmount}(); + wip.deposit{ value: depositAmount }(); uint256 balanceBeforeWithdraw = address(this).balance; @@ -121,4 +121,3 @@ contract WIPTest is Test { receive() external payable {} } -