Skip to content

Commit 686c0d2

Browse files
Merge pull request #1 from piplabs/wip-impl
Introduce "Wrapped IP" Token
2 parents 244cab6 + dc416f9 commit 686c0d2

File tree

10 files changed

+1107
-45
lines changed

10 files changed

+1107
-45
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
STORY_PRIVATEKEY = 0x12341234123412341234123412341234

.github/workflows/test.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ jobs:
2929
run: |
3030
forge --version
3131
32-
- name: Run Forge fmt
33-
run: |
34-
forge fmt --check
35-
id: fmt
36-
3732
- name: Run Forge build
3833
run: |
3934
forge build --sizes

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ out/
44

55
# Ignores development broadcast logs
66
!/broadcast
7+
/broadcast/
78
/broadcast/*/31337/
89
/broadcast/**/dry-run/
910

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
## Foundry
1+
# Wrapped IP
22

3-
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
3+
The "Wrapped IP" refer to WETH-9 with additional features through relatively minor changes.
44

5-
Foundry consists of:
5+
## Deployments
6+
[STORY Odyssey Testnet](https://internal.storyscan.xyz/address/0xfa057f2e7515267ffab367d0a769f3fa1489b869) `0xFA057f2e7515267FFAB367D0a769F3Fa1489b869`
67

7-
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
8-
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
9-
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
10-
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
118

12-
## Documentation
13-
14-
https://book.getfoundry.sh/
15-
16-
## Usage
9+
## Features
10+
- Supports [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface detection.
11+
- Supports [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) signed approvals.
12+
- Supports [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) contract signature verification.
13+
- Prevents from burning or sending WIP tokens to the contract.
1714

1815
### Build
1916

@@ -27,40 +24,19 @@ $ forge build
2724
$ forge test
2825
```
2926

30-
### Format
31-
32-
```shell
33-
$ forge fmt
34-
```
35-
36-
### Gas Snapshots
37-
38-
```shell
39-
$ forge snapshot
40-
```
41-
42-
### Anvil
43-
27+
### Deploy on Story Odyssey Testnet
28+
Create a `.env` file with the following content:
4429
```shell
45-
$ anvil
30+
STORY_PRIVATEKEY = <private_key of wallet address to execute command below>
4631
```
47-
48-
### Deploy
32+
you can also refer to `.env.example` file for reference.
4933

5034
```shell
51-
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
35+
export STORY_PRIVATE_KEY=<private_key>
36+
$ forge script script/Deploy.s.sol:Deploy --fork-url https://odyssey.storyrpc.io/ -v --broadcast --sender <wallet address> --priority-gas-price 1 --slow --legacy --skip-simulation --verify --verifier=blockscout --verifier-url=https://internal.storyscan.xyz/api
5237
```
38+
5339

54-
### Cast
5540

56-
```shell
57-
$ cast <subcommand>
58-
```
5941

60-
### Help
6142

62-
```shell
63-
$ forge --help
64-
$ anvil --help
65-
$ cast --help
66-
```

foundry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
optimizer = true
6+
optimizer_runs = 20000
57

68
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

gas-report.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
| src/WIP.sol:WIP contract | |
2+
|--------------------------|-----------------|
3+
| Deployment Cost | Deployment Size |
4+
| 1318220 | 6020 |
5+
6+
| Function Name | min | avg | median | max | # calls |
7+
|--------------------------|-----------------|-------|--------|-------|---------|
8+
| balanceOf | 552 | 1350 | 552 | 2552 | 1287 |
9+
| decimals | 2402 | 2402 | 2402 | 2402 | 1 |
10+
| deposit | 25034 | 44664 | 44934 | 44934 | 516 |
11+
| name | 582 | 582 | 582 | 582 | 1 |
12+
| receive | 24885 | 44475 | 44785 | 44785 | 257 |
13+
| symbol | 625 | 625 | 625 | 625 | 1 |
14+
| totalSupply | 273 | 273 | 273 | 273 | 1286 |
15+
| withdraw | 25562 | 34528 | 35086 | 35206 | 259 |

script/Deploy.s.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.26;
3+
4+
import { Script, console } from "forge-std/Script.sol";
5+
import { WIP } from "../src/WIP.sol";
6+
7+
contract Deploy is Script {
8+
WIP public wip;
9+
10+
function setUp() public {}
11+
12+
function run() public {
13+
vm.startBroadcast(vm.envUint("STORY_PRIVATEKEY"));
14+
wip = new WIP();
15+
vm.stopBroadcast();
16+
}
17+
}

src/WIP.sol

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (C) 2015, 2016, 2017 Dapphub
3+
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
pragma solidity 0.8.26;
18+
19+
interface IWIP {
20+
function deposit() external payable;
21+
function withdraw(uint wad) external;
22+
23+
event Deposit(address indexed dst, uint wad);
24+
event Withdrawal(address indexed src, uint wad);
25+
26+
error WIP_IPTransferFailed();
27+
error WIP_InvalidSignature();
28+
error WIP_ExpiredSignature();
29+
error WIP_InvalidTransferRecipient();
30+
31+
// ERC20
32+
function name() external view returns (string memory);
33+
function symbol() external view returns (string memory);
34+
function decimals() external view returns (uint8);
35+
36+
function totalSupply() external view returns (uint);
37+
function balanceOf(address guy) external view returns (uint);
38+
function allowance(address src, address dst) external view returns (uint);
39+
40+
function approve(address spender, uint wad) external returns (bool);
41+
function transfer(address dst, uint wad) external returns (bool);
42+
function transferFrom(address src, address dst, uint wad) external returns (bool);
43+
44+
event Approval(address indexed src, address indexed dst, uint wad);
45+
event Transfer(address indexed src, address indexed dst, uint wad);
46+
47+
// ERC-165
48+
function supportsInterface(bytes4 interfaceID) external view returns (bool);
49+
50+
// ERC-2612
51+
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
52+
function nonces(address owner) external view returns (uint);
53+
function DOMAIN_SEPARATOR() external view returns (bytes32);
54+
55+
// Permit2
56+
function permit2(address owner, address spender, uint amount, uint deadline, bytes calldata signature) external;
57+
}
58+
59+
contract WIP is IWIP {
60+
string public constant override name = "Wrapped IP";
61+
string public constant override symbol = "WIP";
62+
uint8 public override decimals = 18;
63+
64+
mapping (address => uint) public override balanceOf;
65+
mapping (address => mapping (address => uint)) public override allowance;
66+
67+
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
68+
bytes32 private constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
69+
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
70+
bytes4 private constant MAGICVALUE = 0x1626ba7e;
71+
mapping(address => uint) public override nonces;
72+
73+
uint private immutable INITIAL_CHAIN_ID;
74+
bytes32 private immutable INITIAL_DOMAIN_SEPARATOR;
75+
76+
constructor() {
77+
INITIAL_CHAIN_ID = block.chainid;
78+
INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
79+
}
80+
81+
receive() external payable {
82+
deposit();
83+
}
84+
85+
function supportsInterface(bytes4 interfaceID) external pure override returns (bool) {
86+
return
87+
// ERC-165
88+
interfaceID == this.supportsInterface.selector ||
89+
// ERC-2612
90+
interfaceID == this.permit.selector ||
91+
// Permit2
92+
interfaceID == this.permit2.selector;
93+
}
94+
95+
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
96+
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
97+
}
98+
99+
function _computeDomainSeparator() private view returns (bytes32) {
100+
return keccak256(
101+
abi.encode(
102+
// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
103+
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
104+
// keccak256(bytes('Wrapped IP')),
105+
0x4a24dd8304360c3edc71acded1e27d8467d787ccaeefb153eaaadce60e21753b,
106+
// keccak256(bytes("1"))
107+
0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6,
108+
block.chainid,
109+
address(this)
110+
)
111+
);
112+
}
113+
114+
function deposit() public payable {
115+
balanceOf[msg.sender] += msg.value;
116+
emit Deposit(msg.sender, msg.value);
117+
}
118+
119+
function withdraw(uint value) external override {
120+
balanceOf[msg.sender] -= value;
121+
(bool success, ) = msg.sender.call{value: value}("");
122+
if (!success) {
123+
revert WIP_IPTransferFailed();
124+
}
125+
emit Withdrawal(msg.sender, value);
126+
}
127+
128+
function totalSupply() external view override returns (uint) {
129+
return address(this).balance;
130+
}
131+
132+
function approve(address spender, uint value) external override returns (bool) {
133+
allowance[msg.sender][spender] = value;
134+
emit Approval(msg.sender, spender, value);
135+
return true;
136+
}
137+
138+
modifier ensuresRecipient(address to) {
139+
// Prevents from burning or sending WIP tokens to the contract.
140+
if (to == address(0)) {
141+
revert WIP_InvalidTransferRecipient();
142+
}
143+
if (to == address(this)) {
144+
revert WIP_InvalidTransferRecipient();
145+
}
146+
_;
147+
}
148+
149+
function transfer(address to, uint value) external ensuresRecipient(to) override returns (bool) {
150+
balanceOf[msg.sender] -= value;
151+
balanceOf[to] += value;
152+
153+
emit Transfer(msg.sender, to, value);
154+
return true;
155+
}
156+
157+
function transferFrom(address from, address to, uint value) external ensuresRecipient(to) override returns (bool) {
158+
if (from != msg.sender) {
159+
uint _allowance = allowance[from][msg.sender];
160+
if (_allowance != type(uint).max) {
161+
allowance[from][msg.sender] -= value;
162+
}
163+
}
164+
165+
balanceOf[from] -= value;
166+
balanceOf[to] += value;
167+
168+
emit Transfer(from, to, value);
169+
return true;
170+
}
171+
172+
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external override {
173+
if (block.timestamp > deadline) {
174+
revert WIP_ExpiredSignature();
175+
}
176+
bytes32 digest = keccak256(
177+
abi.encodePacked(
178+
'\x19\x01',
179+
DOMAIN_SEPARATOR(),
180+
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
181+
)
182+
);
183+
address recoveredAddress = ecrecover(digest, v, r, s);
184+
if (recoveredAddress != owner) {
185+
revert WIP_InvalidSignature();
186+
}
187+
if (recoveredAddress == address(0)) {
188+
revert WIP_InvalidSignature();
189+
}
190+
allowance[owner][spender] = value;
191+
emit Approval(owner, spender, value);
192+
}
193+
194+
function permit2(address owner, address spender, uint value, uint deadline, bytes calldata signature) external override {
195+
if (block.timestamp > deadline) {
196+
revert WIP_ExpiredSignature();
197+
}
198+
bytes32 digest = keccak256(
199+
abi.encodePacked(
200+
'\x19\x01',
201+
DOMAIN_SEPARATOR(),
202+
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
203+
)
204+
);
205+
if (!_checkSignature(owner, digest, signature)) {
206+
revert WIP_InvalidSignature();
207+
}
208+
allowance[owner][spender] = value;
209+
emit Approval(owner, spender, value);
210+
}
211+
212+
function _checkSignature(address signer, bytes32 hash, bytes memory signature) private view returns (bool) {
213+
(address recoveredAddress) = _recover(hash, signature);
214+
if (recoveredAddress == signer) {
215+
if (recoveredAddress != address(0)) {
216+
return true;
217+
}
218+
}
219+
220+
(bool success, bytes memory result) = signer.staticcall(
221+
abi.encodeWithSelector(MAGICVALUE, hash, signature)
222+
);
223+
return (
224+
success &&
225+
result.length == 32 &&
226+
abi.decode(result, (bytes32)) == bytes32(MAGICVALUE)
227+
);
228+
}
229+
230+
function _recover(bytes32 hash, bytes memory signature) private pure returns (address) {
231+
if (signature.length != 65) {
232+
return address(0);
233+
}
234+
235+
bytes32 r;
236+
bytes32 s;
237+
uint8 v;
238+
239+
assembly {
240+
r := mload(add(signature, 0x20))
241+
s := mload(add(signature, 0x40))
242+
v := byte(0, mload(add(signature, 0x60)))
243+
}
244+
245+
if (uint(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
246+
return address(0);
247+
}
248+
249+
return ecrecover(hash, v, r, s);
250+
}
251+
}

0 commit comments

Comments
 (0)