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.