| jmcph4 |

Eth to Cairo, Part 1: A Misadventure With Transpilation

[2022-11-21 16:00:00 +1000]

Let's put Zora on Starknet.

At the moment you need to use Cairo for this. Warp is a transpiler from Solidity to Cairo. It looks like this:

$$ \text{warp}:\text{Solidity}\rightarrow\text{Cairo} $$

Nice; how's it work?

$ warp transpile /path/to/your/solidity/codebase/Foobar.sol

Woah, this is going to be easy. How do we install it?

$ git clone git@github.com:NethermindEth/warp.git && cd warp && wc -l readme.md | cut -f 1 -d ' '
357

Wow! That's a lot of steps! The time saved could be huge though, so I guess it's worth it.

*Follows README installation steps*

Phew, glad that's done. Let's transpile!

$ git clone git@github.com:ourzora/v3.git && cd v3
$ warp transpile contracts/ZoraModuleManager.sol
---Compile Failed---
Compiler version 0.8.14 reported errors:
    --1--
    ParserError: Source file requires different compiler version (current compiler is 0.8.14+commit.de5c19a6.Linux.g++) - note that nightly builds are considered to be strictly less than the released version
     --> contracts/ZoraModuleManager.sol:2:1:
      |
    2 | pragma solidity 0.8.10;
      | ^^^^^^^^^^^^^^^^^^^^^^^


Cannot start transpilation

Ah, wrong Solidity version; bound to happen. I guess warp allows us to specify the Solidity version we want to use, or alternatively just provide it a path to a solc binary we can use?

$ warp
Usage: warp [options] [command]

Options:
  -h, --help                          display help for command

Commands:
  transpile [options] <files...>
  transform [options] <file>
  test [options]
  analyse [options] <file>
  status [options] <tx_hash>
  compile [options] <file>
  deploy [options] <file>
  deploy_account [options]
  invoke [options] <file>
  call [options] <file>
  install [options]
  declare [options] <cairo_contract>  Command to declare Cairo contract on a StarkNet Network.
  version
  help [command]                      display help for command
$ warp help transpile
Usage: warp transpile [options] <files...>

Options:
  --compile-cairo
  --no-compile-errors
  --check-trees
  --highlight <ids...>
  --order <passOrder>
  -o, --output-dir <path>  Output directory for transpiled Cairo files. (default: "warp_output")
  -d, --debug-info         Include debug information. (default: false)
  --print-trees
  --no-result
  --no-stubs
  --no-strict
  --until <pass>
  --no-warnings
  --dev                    Run AST sanity checks on every pass instead of the final AST only (default: false)
  -h, --help               display help for command

Huh, nothing here. Maybe the warp compile subcommand has something and it's just been omitted from the help text for wrap transpile?

$ warp help compile
Usage: warp compile [options] <file>

Options:
  -d, --debug-info  Include debug information. (default: false)
  -h, --help        display help for command

Okay, don't panic. Surely the source will shed some light on what's going on here?

$ cd /path/to/warp/source
$ ls src
2110008 4.0K drwxr-xr-x  8 jmcph4 jmcph4 4.0K Nov 21 01:36 .
2109932 4.0K drwxr-xr-x 17 jmcph4 jmcph4 4.0K Nov 21 01:37 ..
2110009 4.0K drwxr-xr-x  3 jmcph4 jmcph4 4.0K Nov 21 01:36 ast
2088607  12K -rw-r--r--  1 jmcph4 jmcph4 9.0K Nov 21 01:36 autoRunSemanticTests.ts
2110011 4.0K drwxr-xr-x  8 jmcph4 jmcph4 4.0K Nov 21 01:36 cairoUtilFuncGen
2088658  44K -rw-r--r--  1 jmcph4 jmcph4  41K Nov 21 01:36 cairoWriter.ts
2088659 4.0K -rw-r--r--  1 jmcph4 jmcph4  405 Nov 21 01:36 export.ts
2088660 4.0K -rw-r--r--  1 jmcph4 jmcph4 2.2K Nov 21 01:36 freeStructWritter.ts
2110019 4.0K drwxr-xr-x  3 jmcph4 jmcph4 4.0K Nov 21 01:36 icf
2088670  12K -rw-r--r--  1 jmcph4 jmcph4  12K Nov 21 01:36 index.ts
2088671 4.0K -rw-r--r--  1 jmcph4 jmcph4 3.4K Nov 21 01:36 io.ts
2088672 4.0K -rw-r--r--  1 jmcph4 jmcph4  918 Nov 21 01:36 nethersolc.ts
2110021 4.0K drwxr-xr-x 16 jmcph4 jmcph4 4.0K Nov 21 01:36 passes
2088796 8.0K -rw-r--r--  1 jmcph4 jmcph4 6.0K Nov 21 01:36 semanticTestRunner.ts
2088797 8.0K -rw-r--r--  1 jmcph4 jmcph4 6.9K Nov 21 01:36 solCompile.ts
2088798 4.0K -rw-r--r--  1 jmcph4 jmcph4 3.2K Nov 21 01:36 solWriter.ts
2088799  12K -rw-r--r--  1 jmcph4 jmcph4 9.1K Nov 21 01:36 starknetCli.ts
2088800  24K -rw-r--r--  1 jmcph4 jmcph4  21K Nov 21 01:36 testing.ts
2088801 8.0K -rw-r--r--  1 jmcph4 jmcph4 7.3K Nov 21 01:36 transpiler.ts
2110037 4.0K drwxr-xr-x  2 jmcph4 jmcph4 4.0K Nov 21 01:36 utils
2110038 4.0K drwxr-xr-x  3 jmcph4 jmcph4 4.0K Nov 21 01:36 warplib
$ less src/nethersolc.ts
import * as os from 'os';
import * as path from 'path';

import { NotSupportedYetError } from './utils/errors';

type SupportedPlatforms = 'linux_x64' | 'darwin_x64' | 'darwin_arm64';
export type SupportedSolcVersions = '7' | '8';

function getPlatform(): SupportedPlatforms {
  const platform = `${os.platform()}_${os.arch()}`;

  switch (platform) {
    case 'linux_x64':
    case 'darwin_x64':
    case 'darwin_arm64':
      return platform;
    default:
      throw new NotSupportedYetError(`Unsupported plaform ${platform}`);
  }
}

export function nethersolcPath(version: SupportedSolcVersions): string {
  const platform = getPlatform();
  return path.resolve(__dirname, '..', 'nethersolc', platform, version, 'solc');
}

export function fullVersionFromMajor(majorVersion: SupportedSolcVersions): string {
  switch (majorVersion) {
    case '7':
      return '0.7.6';
    case '8':
      return '0.8.14';
  }
}

Whack. Alas, we're a lot harder to knock over than that! We can trick warp into using a different Solidity compiler because it just vendors the raw binary anyway. Let's acquire a binary of the Solidity compiler for the version Zora uses: 0.8.10. Fortunately, the Ethereum Foundation maintains a repository of exactly this for each Solidity version.

$ curl --output solc810 https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/linux-amd64/solc-linux-amd64-v0.8.10%2Bcommit.fc410830
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 13.1M  100 13.1M    0     0  28.0M      0 --:--:-- --:--:-- --:--:-- 28.2M
$ chmod u+x solc810

Now we need to find where on Earth warp loads its vendored compiler from.

$ which warp
/home/jmcph4/.yarn/bin/warp
$ cd /home/jmcph4/.yarn/bin
$ ls
1558195 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 01:32 .
1558194 4.0K drwxr-xr-x 3 jmcph4 jmcph4 4.0K Jul  8  2021 ..
1534712    0 lrwxrwxrwx 1 jmcph4 jmcph4   50 Nov 21 01:32 ethers -> ../../.config/yarn/global/node_modules/.bin/ethers
1535269    0 lrwxrwxrwx 1 jmcph4 jmcph4   56 Nov 21 01:32 ethers-build -> ../../.config/yarn/global/node_modules/.bin/ethers-build
1535270    0 lrwxrwxrwx 1 jmcph4 jmcph4   57 Nov 21 01:32 ethers-deploy -> ../../.config/yarn/global/node_modules/.bin/ethers-deploy
1564459    0 lrwxrwxrwx 1 jmcph4 jmcph4   48 Nov 21 01:32 warp -> ../../.config/yarn/global/node_modules/.bin/warp
$ cd ../../.config/yarn/global/node_modules/.bin
$ ls
total 24K
2654710 4.0K drwxr-xr-x   2 jmcph4 jmcph4 4.0K Nov 21 01:32 .
2608393  20K drwxr-xr-x 586 jmcph4 jmcph4  20K Nov 21 01:32 ..
2615429    0 lrwxrwxrwx   1 jmcph4 jmcph4   24 Nov 21 01:32 ethers -> ../ethers-cli/bin/ethers
2615430    0 lrwxrwxrwx   1 jmcph4 jmcph4   30 Nov 21 01:32 ethers-build -> ../ethers-cli/bin/ethers-build
2615431    0 lrwxrwxrwx   1 jmcph4 jmcph4   34 Nov 21 01:32 ethers-deploy -> ../ethers-cli/bin/ethers-deploy.js
2615428    0 lrwxrwxrwx   1 jmcph4 jmcph4   31 Nov 21 01:32 warp -> ../@nethermindeth/warp/bin/warp
$ cd ../@nethermindeth/warp
$ ls
total 68K
2654258 4.0K drwxr-xr-x 9 jmcph4 jmcph4 4.0K Nov 21 01:33 .
2654246 4.0K drwxr-xr-x 3 jmcph4 jmcph4 4.0K Nov 21 01:32 ..
2654378 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 01:32 bin
2654379 4.0K drwxr-xr-x 7 jmcph4 jmcph4 4.0K Nov 21 01:32 build
2629586  12K -rw-r--r-- 1 jmcph4 jmcph4  12K Nov 21 01:32 LICENSE
2654380 4.0K drwxr-xr-x 5 jmcph4 jmcph4 4.0K Nov 21 01:32 nethersolc
2654701 4.0K drwxr-xr-x 3 jmcph4 jmcph4 4.0K Nov 21 01:32 node_modules
2629597 4.0K -rw-r--r-- 1 jmcph4 jmcph4 2.9K Nov 21 01:32 package.json
2629599  12K -rw-r--r-- 1 jmcph4 jmcph4 8.8K Nov 21 01:32 readme.md
2654381 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 01:32 starknet-scripts
2654382 4.0K drwxr-xr-x 3 jmcph4 jmcph4 4.0K Nov 21 01:32 warplib
2654711 4.0K drwxr-xr-x 6 jmcph4 jmcph4 4.0K Nov 21 01:34 warp_venv
2629622 4.0K -rwxr-xr-x 1 jmcph4 jmcph4  225 Nov 21 01:32 warp_venv.sh
$ cd nethersolc
$ ls
total 20K
2654380 4.0K drwxr-xr-x 5 jmcph4 jmcph4 4.0K Nov 21 01:32 .
2654258 4.0K drwxr-xr-x 9 jmcph4 jmcph4 4.0K Nov 21 01:33 ..
2654581 4.0K drwxr-xr-x 4 jmcph4 jmcph4 4.0K Nov 21 01:32 darwin_arm64
2654582 4.0K drwxr-xr-x 4 jmcph4 jmcph4 4.0K Nov 21 01:32 darwin_x64
2654583 4.0K drwxr-xr-x 4 jmcph4 jmcph4 4.0K Nov 21 01:32 linux_x64
$ cd linux_x64
$ ls
total 16K
2654583 4.0K drwxr-xr-x 4 jmcph4 jmcph4 4.0K Nov 21 01:32 .
2654380 4.0K drwxr-xr-x 5 jmcph4 jmcph4 4.0K Nov 21 01:32 ..
2654658 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 01:32 7
2654659 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 03:54 8
$ cd 8
$ ls
total 16M
2654659 4.0K drwxr-xr-x 2 jmcph4 jmcph4 4.0K Nov 21 04:11 .
2654583 4.0K drwxr-xr-x 4 jmcph4 jmcph4 4.0K Nov 21 01:32 ..
2632815  16M -rwxr-xr-x 1 jmcph4 jmcph4  16M Nov 21 01:32 solc

They don't make it easy! Time for the ole' switcheroo:

$ rm solc
$ cp /path/to/our/desired/solc/binary/solc .
$ ./solc --version
Version: 0.8.10+commit.fc410830.Linux.g++

That was exhausting -- let's try and transpile again!

$ cd /path/to/zora/v3
$ warp transpile contracts/ZoraModuleManager.sol
---Compile Failed---
Compiler version 0.8.14 reported errors:
    --1--
    ParserError: Source "@openzeppelin/contracts/token/ERC721/ERC721.sol" not found: File not found.
     --> contracts/auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol:4:1:
      |
    4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
      | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Cannot start transpilation

Of course. In the fray, we've neglected the actual Zora dependencies. Let's just guess it's a Yarn thing given the syntax they've used.

$ yarn install
yarn install v1.22.10
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning " > @typechain/ethers-v5@10.1.0" has unmet peer dependency "@ethersproject/abi@^5.0.0".
warning " > @typechain/ethers-v5@10.1.0" has unmet peer dependency "@ethersproject/bytes@^5.0.0".
warning " > @typechain/ethers-v5@10.1.0" has unmet peer dependency "@ethersproject/providers@^5.0.0".
warning " > @typechain/ethers-v5@10.1.0" has unmet peer dependency "ethers@^5.1.3".
[4/4] Building fresh packages...
Done in 7.24s.

This alone is still insufficient, though. Because we're not using Foundry at all, we'll basically need to manually hack our own remapping feature. Not to worry:

$ ln -s node_modules/@openzeppelin @openzeppelin
$ warp transpile contracts/ZoraModuleManager.sol
---Compile Failed---
Compiler version 0.8.14 reported errors:
    --1--
    ParserError: Source "@openzeppelin/contracts/token/ERC721/ERC721.sol" not found: File not found.
     --> contracts/auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol:4:1:
      |
    4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
      | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Cannot start transpilation
jmcph4@dunnart:~/dev/tmp/v3$ ln -s node_modules/@openzeppelin @openzeppelin
jmcph4@dunnart:~/dev/tmp/v3$ warp transpile contracts/ZoraModuleManager.sol
Transpilation abandoned Detected 21 Unsupported Features:

File @openzeppelin/contracts/token/ERC721/ERC721.sol:

1. Conditional expressions (ternary operator, node) are not supported:

	................
92	   function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
93	       require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
94
95	       string memory baseURI = _baseURI();
96	       return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
97	   }
98
99	   /**


2. Try/Catch statements are not supported:

	................
390	       uint256 tokenId,
391	       bytes memory _data
392	   ) private returns (bool) {
393	       if (to.isContract()) {
394	           try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
395	               return retval == IERC721Receiver.onERC721Received.selector;
396	           } catch (bytes memory reason) {
397	               if (reason.length == 0) {
398	                   revert("ERC721: transfer to non ERC721Receiver implementer");
399	               } else {
400	                   assembly {
401	                       revert(add(32, reason), mload(reason))
402	                   }
403	               }
404	           }
405	       } else {
406	           return true;
407	       }


File @openzeppelin/contracts/token/ERC721/IERC721.sol:

3. Indexed parameters are not supported:

	................
10	interface IERC721 is IERC165 {
11	   /**
12	    * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
13	    */
14	   event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
15
16	   /**
17	    * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.


4. Indexed parameters are not supported:

	................
15
16	   /**
17	    * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
18	    */
19	   event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
20
21	   /**
22	    * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.


5. Indexed parameters are not supported:

	................
20
21	   /**
22	    * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
23	    */
24	   event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
25
26	   /**
27	    * @dev Returns the number of tokens in ``owner``'s account.


File @openzeppelin/contracts/utils/Address.sol:

6. Members of addresses are not supported. Found at MemberAccess #1783:

	................
36	       // This method relies on extcodesize/address.code.length, which returns 0
37	       // for contracts in construction, since the code is only stored at the end
38	       // of the constructor execution.
39
40	       return account.code.length > 0;
41	   }
42
43	   /**


7. Members of addresses are not supported. Found at MemberAccess #1802:

	................
56	    * {ReentrancyGuard} or the
57	    * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
58	    */
59	   function sendValue(address payable recipient, uint256 amount) internal {
60	       require(address(this).balance >= amount, "Address: insufficient balance");
61
62	       (bool success, ) = recipient.call{value: amount}("");
63	       require(success, "Address: unable to send value, recipient may have reverted");


8. Function call options (other than `salt` when creating a contract), such as {gas:X} and {value:X} are not supported:

	................
58	    */
59	   function sendValue(address payable recipient, uint256 amount) internal {
60	       require(address(this).balance >= amount, "Address: insufficient balance");
61
62	       (bool success, ) = recipient.call{value: amount}("");
63	       require(success, "Address: unable to send value, recipient may have reverted");
64	   }
65


9. Members of addresses are not supported. Found at MemberAccess #1899:

	................
129	       bytes memory data,
130	       uint256 value,
131	       string memory errorMessage
132	   ) internal returns (bytes memory) {
133	       require(address(this).balance >= value, "Address: insufficient balance for call");
134	       require(isContract(target), "Address: call to non-contract");
135
136	       (bool success, bytes memory returndata) = target.call{value: value}(data);


10. Function call options (other than `salt` when creating a contract), such as {gas:X} and {value:X} are not supported:

	................
132	   ) internal returns (bytes memory) {
133	       require(address(this).balance >= value, "Address: insufficient balance for call");
134	       require(isContract(target), "Address: call to non-contract");
135
136	       (bool success, bytes memory returndata) = target.call{value: value}(data);
137	       return verifyCallResult(success, returndata, errorMessage);
138	   }
139


11. Members of addresses are not supported. Found at MemberAccess #1971:

	................
159	       string memory errorMessage
160	   ) internal view returns (bytes memory) {
161	       require(isContract(target), "Address: static call to non-contract");
162
163	       (bool success, bytes memory returndata) = target.staticcall(data);
164	       return verifyCallResult(success, returndata, errorMessage);
165	   }
166


12. Members of addresses are not supported. Found at MemberAccess #2023:

	................
186	       string memory errorMessage
187	   ) internal returns (bytes memory) {
188	       require(isContract(target), "Address: delegate call to non-contract");
189
190	       (bool success, bytes memory returndata) = target.delegatecall(data);
191	       return verifyCallResult(success, returndata, errorMessage);
192	   }
193


13. Yul blocks are not supported:

	................
208	           // Look for revert reason and bubble it up if present
209	           if (returndata.length > 0) {
210	               // The easiest way to bubble the revert reason is using memory via assembly
211
212	               assembly {
213	                   let returndata_size := mload(returndata)
214	                   revert(add(32, returndata), returndata_size)
215	               }
216	           } else {
217	               revert(errorMessage);
218	           }


File @openzeppelin/contracts/utils/Context.sol:

14. msg object not supported outside of 'msg.sender':

	................
17	       return msg.sender;
18	   }
19
20	   function _msgData() internal view virtual returns (bytes calldata) {
21	       return msg.data;
22	   }
23	}
24

File contracts/ZoraModuleManager.sol:

15. Indexed parameters are not supported:

	................
48	   /// @notice Emitted when a user's module approval is updated
49	   /// @param user The address of the user
50	   /// @param module The address of the module
51	   /// @param approved Whether the user added or removed approval
52	   event ModuleApprovalSet(address indexed user, address indexed module, bool approved);
53
54	   /// @notice Emitted when a module is registered
55	   /// @param module The address of the module


16. Indexed parameters are not supported:

	................
52	   event ModuleApprovalSet(address indexed user, address indexed module, bool approved);
53
54	   /// @notice Emitted when a module is registered
55	   /// @param module The address of the module
56	   event ModuleRegistered(address indexed module);
57
58	   /// @notice Emitted when the registrar address is updated
59	   /// @param newRegistrar The address of the new registrar


17. Indexed parameters are not supported:

	................
56	   event ModuleRegistered(address indexed module);
57
58	   /// @notice Emitted when the registrar address is updated
59	   /// @param newRegistrar The address of the new registrar
60	   event RegistrarChanged(address indexed newRegistrar);
61
62	   /// @param _registrar The initial registrar for the manager
63	   /// @param _feeToken The module fee token contract to mint from upon module registration


18. Yul blocks are not supported:

	................
299	   }
300
301	   /// @notice The EIP-155 chain id
302	   function _chainID() private view returns (uint256 id) {
303	       assembly {
304	           id := chainid()
305	       }
306	   }
307	}
308

File contracts/auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol:

19. Indexed parameters are not supported:

	................
40	   /// @notice Emitted when the fee for a module is updated
41	   /// @param module The address of the module
42	   /// @param feeRecipient The address of the fee recipient
43	   /// @param feeBps The basis points of the fee
44	   event ProtocolFeeUpdated(address indexed module, address feeRecipient, uint16 feeBps);
45
46	   /// @notice Emitted when the contract metadata is updated
47	   /// @param newMetadata The address of the new metadata


20. Indexed parameters are not supported:

	................
44	   event ProtocolFeeUpdated(address indexed module, address feeRecipient, uint16 feeBps);
45
46	   /// @notice Emitted when the contract metadata is updated
47	   /// @param newMetadata The address of the new metadata
48	   event MetadataUpdated(address indexed newMetadata);
49
50	   /// @notice Emitted when the contract owner is updated
51	   /// @param newOwner The address of the new owner


21. Indexed parameters are not supported:

	................
48	   event MetadataUpdated(address indexed newMetadata);
49
50	   /// @notice Emitted when the contract owner is updated
51	   /// @param newOwner The address of the new owner
52	   event OwnerUpdated(address indexed newOwner);
53
54	   constructor() ERC721("ZORA Module Fee Switch", "ZORF") {
55	       _setOwner(msg.sender);

Well, I guess we should have studied the Warp README more thoroughly. Turns out the Zora codebase uses some features Warp doesn't support yet.

After all of that, it looks like we'll have to do this ourselves the old fashioned way.