├── .github └── workflows │ └── test.yaml ├── .gitignore ├── README.md ├── ape-config.yaml ├── contracts ├── WETH8.vy └── test │ └── TestFlashReceiver.vy ├── requirements.txt ├── scripts ├── __init__.py └── deploy.py └── tests ├── conftest.py ├── test_erc2612.py ├── test_erc3156.py └── test_weth.py /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: Test 4 | 5 | jobs: 6 | functional: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: ApeWorX/github-action@v1 11 | - run: ape compile --size 12 | - run: ape test -s 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ape stuff 2 | .build/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wrapped Ether (WETH8) 2 | 3 | This experiment updates the canonical ["Wrapped Ether" WETH(9) contract](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code) 4 | by implementing it in modern Vyper with several improvements for gas savings purposes. 5 | We also compare it to the [WETH(10)](https://github.com/WETH10/WETH10) project as another attempt to make an upgrade to WETH(9). 6 | 7 | ## Wrapping Ether 8 | 9 | Any operation that ends with this contract holding Wrapped Ether is prohibited. 10 | 11 | `deposit` Ether in this contract to receive Wrapped Ether (WETH), which implements the ERC20 standard. WETH is interchangeable with Ether in a 1:1 basis. 12 | 13 | `withdraw` Ether from this contract by unwrapping WETH from your wallet. 14 | 15 | ## Approvals 16 | 17 | When an account's `allowance` is set to `max(uint256)` it will not decrease through `transferFrom` or `withdrawFrom` calls. 18 | 19 | WETH10 implements EIP2612 to set approvals through off-chain signatures. 20 | 21 | ## Why WETH(8)? 22 | 23 | The original WETH(9) contract uses "Kelvin Versioning", which is a unique way to do software versioning such that versions start at a high number 24 | and count down to version 0, at which point the software is considered finished and no further modifications are made. 25 | In the spirit of that original, we are decrementing this number by 1 to indicate the level of progress this implementation is making towards a 26 | final, more immutable copy. 27 | 28 | ## Deployments 29 | 30 | This contract is deployed on Goerli at: [0xD2082D10e36b169f4F2331867dcc1A719297037e](https://goerli.etherscan.io/address/0xd2082d10e36b169f4f2331867dcc1a719297037e) 31 | 32 | ## Credits 33 | 34 | This project draws singificant inspiration from WETH(10) both in terms of goals and several of the upgrades that are applied. 35 | 36 | Generated from [Token template](https://github.com/ApeAcademy/ERC20) by [Ape Academy](https://academy.apeworx.io) 37 | -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | name: WETH8 2 | plugins: 3 | # So we can compile WETH9 and WETH10 4 | - name: solidity 5 | # So we can compile our project 6 | - name: vyper 7 | 8 | dependencies: 9 | - name: weth9 10 | github: tongyuhu/weth9 11 | branch: master 12 | - name: weth10 13 | github: WETH10/WETH10 14 | branch: main 15 | -------------------------------------------------------------------------------- /contracts/WETH8.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | """ 3 | @title WETH8 4 | @license MIT 5 | @author ApeWorX Ltd. 6 | @notice Vyper implementation of the WETH9 contract + ERC2612 + ERC3156 7 | """ 8 | 9 | from vyper.interfaces import ERC20 10 | 11 | 12 | implements: ERC20 13 | 14 | # ERC20 Token Metadata 15 | name: public(constant(String[20])) = "Wrapped Ether" 16 | symbol: public(constant(String[5])) = "WETH" 17 | decimals: public(constant(uint8)) = 18 18 | 19 | # ERC20 State Variables 20 | balanceOf: public(HashMap[address, uint256]) 21 | allowance: public(HashMap[address, HashMap[address, uint256]]) 22 | 23 | # ERC20 Events 24 | event Transfer: 25 | sender: indexed(address) 26 | receiver: indexed(address) 27 | amount: uint256 28 | 29 | event Approval: 30 | owner: indexed(address) 31 | spender: indexed(address) 32 | amount: uint256 33 | 34 | # EIP-2612 35 | nonces: public(HashMap[address, uint256]) 36 | DOMAIN_SEPARATOR: public(bytes32) 37 | DOMAIN_TYPE_HASH: constant(bytes32) = keccak256( 38 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 39 | ) 40 | PERMIT_TYPE_HASH: constant(bytes32) = keccak256( 41 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 42 | ) 43 | 44 | # Flash Minting 45 | 46 | interface FlashBorrower: 47 | def onFlashLoan( 48 | caller: address, 49 | token: address, 50 | amount: uint256, 51 | fee: uint256, 52 | data: Bytes[65535], 53 | ) -> bytes32: nonpayable 54 | 55 | ERC3156_CALLBACK_SUCCESS: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 56 | 57 | flashMinted: public(uint256) 58 | 59 | 60 | @external 61 | def __init__(): 62 | # EIP-2612 63 | self.DOMAIN_SEPARATOR = keccak256( 64 | concat( 65 | DOMAIN_TYPE_HASH, 66 | keccak256(name), 67 | keccak256("1"), 68 | _abi_encode(chain.id, self) 69 | ) 70 | ) 71 | 72 | 73 | @view 74 | @external 75 | def totalSupply() -> uint256: 76 | # NOTE: Safe because ether balance is <<< max(uint256) 77 | # and `flashMinted` is limited to 1% of total ether 78 | return unsafe_add(self.balance, self.flashMinted) 79 | 80 | 81 | @external 82 | def transfer(receiver: address, amount: uint256) -> bool: 83 | assert receiver not in [empty(address), self] 84 | 85 | self.balanceOf[msg.sender] -= amount 86 | # NOTE: `unsafe_add` is safe here because there is a limited amount of ether <<< max(uint256) 87 | self.balanceOf[receiver] = unsafe_add(self.balanceOf[receiver], amount) 88 | 89 | log Transfer(msg.sender, receiver, amount) 90 | return True 91 | 92 | 93 | @internal 94 | def _setAllowance(owner: address, spender: address, allowance: uint256): 95 | self.allowance[owner][spender] = allowance 96 | log Approval(owner, spender, allowance) 97 | 98 | 99 | @external 100 | def transferFrom(sender: address, receiver: address, amount: uint256) -> bool: 101 | assert receiver not in [empty(address), self] 102 | 103 | allowance: uint256 = self.allowance[sender][msg.sender] 104 | if allowance < max_value(uint256): 105 | # NOTE: Only decrement if less than `max(uint256)` 106 | self._setAllowance(sender, msg.sender, allowance - amount) 107 | 108 | self.balanceOf[sender] -= amount 109 | # NOTE: `unsafe_add` is safe here because of total supply invariant 110 | self.balanceOf[receiver] = unsafe_add(self.balanceOf[receiver], amount) 111 | 112 | log Transfer(sender, receiver, amount) 113 | return True 114 | 115 | 116 | @external 117 | def approve(spender: address, amount: uint256) -> bool: 118 | """ 119 | @param spender The address that will execute on owner behalf. 120 | @param amount The amount of token to be transfered. 121 | @return A boolean that indicates if the operation was successful. 122 | """ 123 | self._setAllowance(msg.sender, spender, amount) 124 | return True 125 | 126 | 127 | @internal 128 | def _burn(owner: address, amount: uint256): 129 | # NOTE: totalSupply decreases here by `amount` 130 | self.balanceOf[owner] -= amount 131 | log Transfer(owner, empty(address), amount) 132 | 133 | 134 | @external 135 | def withdraw(amount: uint256) -> bool: 136 | """ 137 | @notice Burns the supplied amount of tokens from the sender wallet. 138 | @param amount The amount of token to be burned. 139 | @return A boolean that indicates if the operation was successful. 140 | """ 141 | self._burn(msg.sender, amount) 142 | send(msg.sender, amount) 143 | return True 144 | 145 | 146 | @internal 147 | def _mint(receiver: address, amount: uint256): 148 | # NOTE: totalSupply increases here by `amount` 149 | # NOTE: `unsafe_add` is safe here because there is a limited amount of ether <<< max(uint256) 150 | self.balanceOf[receiver] = unsafe_add(self.balanceOf[receiver], amount) 151 | log Transfer(empty(address), receiver, amount) 152 | 153 | 154 | @payable 155 | @external 156 | def deposit() -> bool: 157 | """ 158 | @notice Function to mint tokens 159 | @return A boolean that indicates if the operation was successful. 160 | """ 161 | self._mint(msg.sender, msg.value) 162 | return True 163 | 164 | 165 | @payable 166 | @external 167 | def __default__(): 168 | self._mint(msg.sender, msg.value) 169 | 170 | 171 | # EIP-2612 172 | @external 173 | def permit( 174 | owner: address, 175 | spender: address, 176 | amount: uint256, 177 | expiry: uint256, 178 | v: uint256, 179 | r: bytes32, 180 | s: bytes32, 181 | ) -> bool: 182 | """ 183 | @notice 184 | Approves spender by owner's signature to expend owner's tokens. 185 | See https://eips.ethereum.org/EIPS/eip-2612. 186 | @param owner The address which is a source of funds and has signed the Permit. 187 | @param spender The address which is allowed to spend the funds. 188 | @param amount The amount of tokens to be spent. 189 | @param expiry The timestamp after which the Permit is no longer valid. 190 | @param v V parameter of secp256k1 signature for Permit by owner. 191 | @param r R parameter of secp256k1 signature for Permit by owner. 192 | @param s S parameter of secp256k1 signature for Permit by owner. 193 | @return A boolean that indicates if the operation was successful. 194 | """ 195 | assert owner != empty(address) # dev: invalid owner 196 | assert expiry == 0 or expiry >= block.timestamp # dev: permit expired 197 | nonce: uint256 = self.nonces[owner] 198 | digest: bytes32 = keccak256( 199 | concat( 200 | b'\x19\x01', 201 | self.DOMAIN_SEPARATOR, 202 | keccak256( 203 | _abi_encode( 204 | PERMIT_TYPE_HASH, 205 | owner, 206 | spender, 207 | amount, 208 | nonce, 209 | expiry, 210 | ) 211 | ) 212 | ) 213 | ) 214 | 215 | assert ecrecover(digest, v, r, s) == owner # dev: invalid signature 216 | 217 | self._setAllowance(owner, spender, amount) 218 | self.nonces[owner] = nonce + 1 219 | 220 | return True 221 | 222 | 223 | @view 224 | @external 225 | def maxFlashLoan(token: address) -> uint256: 226 | if token != self: 227 | return 0 228 | 229 | return self.balance / 100 # 1% of total supply 230 | 231 | 232 | @view 233 | @external 234 | def flashFee(token: address, amount: uint256) -> uint256: 235 | assert token == self 236 | return 0 237 | 238 | 239 | @external 240 | def flashLoan(receiver: address, token: address, amount: uint256, data: Bytes[65535]) -> bool: 241 | assert token == self 242 | minted: uint256 = self.flashMinted 243 | # NOTE: Can't violate minting invariant of 1% 244 | assert minted + amount <= self.balance / 100 245 | 246 | self._mint(receiver, amount) 247 | self.flashMinted = minted + amount 248 | 249 | assert FlashBorrower(receiver).onFlashLoan(msg.sender, self, amount, 0, data) == ERC3156_CALLBACK_SUCCESS 250 | 251 | # NOTE: According to ERC3156, `receiver` must set an approval for this contract 252 | allowance: uint256 = self.allowance[receiver][self] 253 | if allowance < max_value(uint256): 254 | # NOTE: Only decrement if less than `max(uint256)` 255 | self._setAllowance(receiver, self, allowance - amount) 256 | 257 | self._burn(receiver, amount) 258 | self.flashMinted = minted 259 | 260 | return True 261 | -------------------------------------------------------------------------------- /contracts/test/TestFlashReceiver.vy: -------------------------------------------------------------------------------- 1 | import WETH8 as WETH8 2 | 3 | ERC3156_CALLBACK_SUCCESS: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 4 | 5 | 6 | @external 7 | def onFlashLoan( 8 | initiator: address, 9 | token: WETH8, 10 | amount: uint256, 11 | fee: uint256, 12 | data: Bytes[65535], 13 | ) -> bytes32: 14 | if len(data) >= 1 and convert(slice(data, 0, 1), bool): 15 | assert token.withdraw(amount) 16 | # NOTE: Don't deposit again, which should fail because it burns 17 | 18 | else: 19 | # NOTE: by default, try withdrawing and depositing again 20 | assert token.withdraw(amount) 21 | assert token.deposit(value=amount) 22 | assert token.approve(msg.sender, amount) 23 | 24 | return ERC3156_CALLBACK_SUCCESS 25 | 26 | 27 | @payable 28 | @external 29 | def __default__(): 30 | pass # So withdrawal doesn't fail 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eip712 -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fubuloubu/WETH8/13145ef35054fb860f821b70788f53d33c213a95/scripts/__init__.py -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | from ape import project 2 | from ape.cli import get_user_selected_account 3 | 4 | 5 | def main(): 6 | account = get_user_selected_account() 7 | account.deploy(project.WETH8) 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from eip712.messages import EIP712Message 3 | 4 | 5 | @pytest.fixture(scope="session") 6 | def deployer(accounts): 7 | return accounts[-1] 8 | 9 | 10 | @pytest.fixture(params=["WETH8", "WETH9", "WETH10"]) 11 | def weth(request, deployer, project): 12 | if request.param == "WETH8": 13 | # Use a type from our project 14 | weth_contract_type = project.WETH8 15 | 16 | else: 17 | # Use a type from a dependency 18 | dependency = list(project.dependencies.get(request.param.lower()).values())[0] 19 | weth_contract_type = dependency.get(request.param) 20 | 21 | # Deploy a contract using our deployer 22 | return deployer.deploy(weth_contract_type) 23 | 24 | 25 | @pytest.fixture 26 | def Permit(chain, weth): 27 | class Permit(EIP712Message): 28 | _name_: "string" = weth.name() 29 | _version_: "string" = "1" 30 | _chainId_: "uint256" = chain.chain_id 31 | _verifyingContract_: "address" = weth.address 32 | 33 | owner: "address" 34 | spender: "address" 35 | value: "uint256" 36 | nonce: "uint256" 37 | deadline: "uint256" 38 | 39 | return Permit 40 | -------------------------------------------------------------------------------- /tests/test_erc2612.py: -------------------------------------------------------------------------------- 1 | import ape 2 | import pytest 3 | 4 | 5 | def test_permit(chain, weth, accounts, Permit): 6 | if weth.contract_type.name == "WETH9": 7 | pytest.skip("WETH9 doesn't support ERC-2612") 8 | 9 | owner, spender = accounts[:2] 10 | 11 | amount = 100 12 | nonce = weth.nonces(owner) 13 | deadline = chain.pending_timestamp + 60 14 | assert weth.allowance(owner, spender) == 0 15 | permit = Permit(owner.address, spender.address, amount, nonce, deadline) 16 | signature = owner.sign_message(permit.signable_message) 17 | v, r, s = signature.v, signature.r, signature.s 18 | 19 | with ape.reverts(): 20 | weth.permit(spender, spender, amount, deadline, v, r, s, sender=spender) 21 | with ape.reverts(): 22 | weth.permit(owner, owner, amount, deadline, v, r, s, sender=spender) 23 | with ape.reverts(): 24 | weth.permit(owner, spender, amount + 1, deadline, v, r, s, sender=spender) 25 | with ape.reverts(): 26 | weth.permit(owner, spender, amount, deadline + 1, v, r, s, sender=spender) 27 | 28 | tx = weth.permit(owner, spender, amount, deadline, v, r, s, sender=spender) 29 | 30 | assert weth.allowance(owner, spender) == 100 31 | 32 | assert tx.events == [weth.Approval(owner, spender, 100)] 33 | -------------------------------------------------------------------------------- /tests/test_erc3156.py: -------------------------------------------------------------------------------- 1 | import ape 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def borrower(project, deployer): 7 | return deployer.deploy(project.TestFlashReceiver) 8 | 9 | 10 | def test_max_flash(weth, accounts): 11 | if weth.contract_type.name == "WETH9": 12 | pytest.skip("WETH9 doesn't support ERC-3156") 13 | 14 | caller = accounts[0] 15 | 16 | if weth.contract_type.name == "WETH10": 17 | assert weth.maxFlashLoan(weth) == (2**112 - 1) # Static value for maximum 18 | 19 | else: 20 | assert weth.maxFlashLoan(weth) == 0 21 | 22 | weth.deposit(sender=caller, value="1 ether") 23 | 24 | assert weth.maxFlashLoan(weth) == int(1e16) # 0.01 ether 25 | 26 | # Says 0 with any address that isn't `weth` 27 | weth.maxFlashLoan(caller) == 0 28 | 29 | 30 | def test_flash_fee(weth, accounts): 31 | if weth.contract_type.name == "WETH9": 32 | pytest.skip("WETH9 doesn't support ERC-3156") 33 | 34 | caller = accounts[0] 35 | 36 | with ape.reverts(): 37 | # Fails with any address that isn't `weth` 38 | weth.flashFee(caller, "1 ether") 39 | 40 | # NOTE: Always no fee 41 | assert weth.flashFee(weth, "1 ether") == 0 42 | 43 | 44 | def test_flash_loan(weth, borrower, accounts): 45 | if weth.contract_type.name == "WETH9": 46 | pytest.skip("WETH9 doesn't support ERC-3156") 47 | 48 | caller = accounts[0] 49 | 50 | with ape.reverts(): 51 | # Fails because no balance 52 | weth.flashLoan(borrower, weth, "0.01 ether", b"", sender=caller) 53 | 54 | weth.deposit(sender=caller, value="1 ether") 55 | 56 | with ape.reverts(): 57 | # Fails because caller doesn't return anything 58 | weth.flashLoan(caller, weth, "0.01 ether", b"", sender=caller) 59 | 60 | with ape.reverts(): 61 | # Fails because caller doesn't have any WETH to give back 62 | # NOTE: nonzero first byte means withdraw but don't deposit 63 | weth.flashLoan(caller, weth, "0.01 ether", b"\x01", sender=caller) 64 | 65 | with ape.reverts(): 66 | # Fails because trying to balance more than available 67 | weth.flashLoan(borrower, weth, "0.011 ether", b"", sender=caller) 68 | 69 | # NOTE: spec requires that the borrower approve the weth address for returning tokens 70 | weth.approve(weth, "0.01 ether", sender=caller) 71 | weth.flashLoan(borrower, weth, "0.01 ether", b"", sender=caller) 72 | -------------------------------------------------------------------------------- /tests/test_weth.py: -------------------------------------------------------------------------------- 1 | import ape 2 | from ape import convert 3 | 4 | # Standard test comes from the interpretation of EIP-20 5 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 6 | ONE_ETHER = convert("1 ether", int) 7 | 8 | 9 | def test_initial_state(weth): 10 | if weth.contract_type.name == "WETH10": 11 | assert weth.name() == "Wrapped Ether v10" 12 | assert weth.symbol() == "WETH10" 13 | else: 14 | assert weth.name() == "Wrapped Ether" 15 | assert weth.symbol() == "WETH" 16 | 17 | assert weth.decimals() == 18 18 | assert weth.totalSupply() == 0 19 | 20 | 21 | def test_deposit_by_call(weth, accounts): 22 | owner = accounts[0] 23 | bal_before = owner.balance 24 | 25 | tx = weth.deposit(sender=owner, value="1 ether") 26 | 27 | assert weth.balance == ONE_ETHER 28 | # assert owner.balance == bal_before - ONE_ETHER 29 | assert weth.balanceOf(owner) == ONE_ETHER 30 | assert weth.totalSupply() == ONE_ETHER 31 | 32 | if weth.contract_type.name == "WETH9": 33 | expected_event = weth.Deposit(owner, ONE_ETHER) 34 | 35 | else: 36 | expected_event = weth.Transfer(ZERO_ADDRESS, owner, ONE_ETHER) 37 | 38 | assert tx.events == [expected_event] 39 | 40 | 41 | def test_deposit_by_send(weth, accounts): 42 | owner = accounts[0] 43 | bal_before = owner.balance 44 | 45 | tx = owner.transfer(weth, "1 ether") 46 | 47 | assert weth.balance == ONE_ETHER 48 | # assert owner.balance == bal_before - ONE_ETHER 49 | assert weth.balanceOf(owner) == ONE_ETHER 50 | assert weth.totalSupply() == ONE_ETHER 51 | 52 | if weth.contract_type.name == "WETH9": 53 | expected_event = weth.Deposit(owner, ONE_ETHER) 54 | 55 | else: 56 | expected_event = weth.Transfer(ZERO_ADDRESS, owner, ONE_ETHER) 57 | 58 | assert tx.events == [expected_event] 59 | 60 | 61 | def test_withdraw(weth, accounts): 62 | owner = accounts[0] 63 | bal_before = owner.balance 64 | 65 | weth.deposit(sender=owner, value="1 ether") 66 | 67 | tx = weth.withdraw(ONE_ETHER, sender=owner) 68 | 69 | assert weth.balance == 0 70 | # assert owner.balance == bal_before 71 | assert weth.balanceOf(owner) == 0 72 | assert weth.totalSupply() == 0 73 | 74 | if weth.contract_type.name == "WETH9": 75 | expected_event = weth.Withdrawal(owner, ONE_ETHER) 76 | 77 | else: 78 | expected_event = weth.Transfer(owner, ZERO_ADDRESS, ONE_ETHER) 79 | 80 | assert tx.events == [expected_event] 81 | 82 | 83 | def test_transfer(weth, accounts): 84 | owner, receiver = accounts[:2] 85 | weth.deposit(sender=owner, value="1 ether") 86 | 87 | tx = weth.transfer(receiver, 100, sender=owner) 88 | 89 | assert weth.balance == ONE_ETHER 90 | assert weth.totalSupply() == ONE_ETHER 91 | assert weth.balanceOf(receiver) == 100 92 | assert weth.balanceOf(owner) == ONE_ETHER - 100 93 | 94 | assert tx.events == [weth.Transfer(owner, receiver, 100)] 95 | 96 | # Expected insufficient funds failure 97 | with ape.reverts(): 98 | weth.transfer(owner, 101, sender=receiver) 99 | 100 | # NOTE: Transfers of 0 values MUST be treated as normal transfers 101 | weth.transfer(owner, 0, sender=owner) 102 | 103 | 104 | def test_approve(weth, accounts): 105 | owner, spender = accounts[:2] 106 | tx = weth.approve(spender, 300, sender=owner) 107 | 108 | assert weth.allowance(owner, spender) == 300 109 | 110 | assert tx.events == [weth.Approval(owner, spender, 300)] 111 | 112 | 113 | def test_transfer_from(weth, accounts): 114 | owner, receiver, spender = accounts[:3] 115 | weth.deposit(sender=owner, value="1 ether") 116 | 117 | # Spender with no approve permission cannot send weths on someone behalf 118 | with ape.reverts(): 119 | weth.transferFrom(owner, receiver, 300, sender=spender) 120 | 121 | # Get approval for allowance from owner 122 | weth.approve(spender, 300, sender=owner) 123 | 124 | # With auth use the allowance to send to receiver via spender(operator) 125 | tx = weth.transferFrom(owner, receiver, 200, sender=spender) 126 | 127 | assert weth.balance == ONE_ETHER 128 | assert weth.allowance(owner, spender) == 100 129 | 130 | if weth.contract_type.name == "WETH9": 131 | assert tx.events == [weth.Transfer(owner, receiver, 200)] 132 | else: 133 | # NOTE: More modern implementations also emit allowance change events 134 | assert tx.events == [ 135 | weth.Approval(owner, spender, 100), 136 | weth.Transfer(owner, receiver, 200), 137 | ] 138 | 139 | # Cannot exceed authorized allowance 140 | with ape.reverts(): 141 | weth.transferFrom(owner, receiver, 200, sender=spender) 142 | 143 | # If approval reset, can't spend anymore 144 | weth.approve(spender, 0, sender=owner) 145 | with ape.reverts(): 146 | weth.transferFrom(owner, receiver, 100, sender=spender) 147 | --------------------------------------------------------------------------------