├── Readme.md └── contracts ├── Implementation.sol └── Proxy.sol /Readme.md: -------------------------------------------------------------------------------- 1 | # Spearbit Writing Exercise 2 | 3 | ## Wallet Protocol 4 | 5 | You are given an implementation for a smart contract wallet. There are two contracts 6 | 7 | 1. [`Implementation.sol`](contracts/Implementation.sol): Deployed once and used as implementation contract in `Proxy.sol`. 8 | 2. [`Proxy.sol`](contracts/Proxy.sol): Each user has a unique `Proxy` deployment with the above implementation. This is a simply proxy contract which delegatecalls the implementation contract. It has an access control check that allows only the owner to use the fallback function. 9 | 10 | The idea is that users can keep their funds, for example, ETH or ERC20 tokens in the Proxy. To use these funds, users can execute arbitrary calls and arbitrary delegatecalls by using the implementation contract (it has `callContract` and `delegatecallContract`). The implementation contract is deployed only once and reused to save gas. 11 | 12 | There is a **critical bug** in the wallet protocol. The exercise is to find it and write it in markdown format, in accordance with the style guide. 13 | 14 | For simplicity, we expect 15 | 16 | ```md 17 | ## Short title for the issue 18 | 19 | **Severity**: High / Medium / Low / Informational / Gas Optimisation 20 | 21 | Context: [`File.sol#L123`](github.com/permalink) 22 | 23 | Description of the attack. 24 | 25 | **Recommendation**: Description on how to avoid the issue. 26 | ``` 27 | 28 | Reference: [Style guide for writing Spearbit reports](https://hackmd.io/@spearbit/S1T63tOqt). 29 | 30 | 31 | *Bonus*: There is simple, yet niche technique to avoid the critical issue by modifying `Implementation.sol`. Bonus point if you can include this in your recommendation. Another bonus point if your recommendation only requires changing a single word in `Implementation.sol` (and removing two more words). 32 | -------------------------------------------------------------------------------- /contracts/Implementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // NOTE: These contracts have a critical bug. 3 | // DO NOT USE THIS IN PRODUCTION. 4 | pragma solidity 0.8.10; 5 | 6 | /// The implementation contract for the Proxy (see: `Proxy.sol`). 7 | /// 8 | /// Only deployed once and the implementation is reused by all proxy contracts. 9 | contract Implementation { 10 | 11 | function callContract(address a, bytes calldata _calldata) payable external returns (bytes memory) { 12 | (bool success , bytes memory ret) = a.call{value: msg.value}(_calldata); 13 | require(success); 14 | return ret; 15 | } 16 | 17 | function delegatecallContract(address a, bytes calldata _calldata) payable external returns (bytes memory) { 18 | (bool success, bytes memory ret) = a.delegatecall(_calldata); 19 | require(success); 20 | return ret; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/Proxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // NOTE: These contracts have a critical bug. 3 | // DO NOT USE THIS IN PRODUCTION 4 | pragma solidity 0.8.10; 5 | 6 | /// A proxy contract inspired by 7 | /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol 8 | /// 9 | /// Only the owner can call the contract, where owner is an immutable variable set during the 10 | /// construction. 11 | /// 12 | /// The implementation will be set to a deployment of `Implementation.sol`. 13 | contract Proxy { 14 | address immutable public owner; 15 | address public implementation; 16 | 17 | constructor(address _implementation, address _owner) { 18 | owner = _owner; 19 | implementation = _implementation; 20 | } 21 | 22 | fallback() external payable { 23 | require(msg.sender == owner); 24 | 25 | address _implementation = implementation; 26 | assembly { 27 | // Copy msg.data. We take full control of memory in this inline assembly 28 | // block because it will not return to Solidity code. We overwrite the 29 | // Solidity scratch space at memory position 0. 30 | calldatacopy(0, 0, calldatasize()) 31 | 32 | // delegatecall the implementation. 33 | // out and outsize are 0 because we don't know the size yet. 34 | let success := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) 35 | 36 | // copy the returned data. 37 | returndatacopy(0, 0, returndatasize()) 38 | 39 | switch success 40 | // delegatecall returns 0 on error. 41 | case 0 { 42 | revert(0, returndatasize()) 43 | } 44 | default { 45 | return(0, returndatasize()) 46 | } 47 | } 48 | } 49 | 50 | receive() external payable { 51 | } 52 | } 53 | --------------------------------------------------------------------------------