create-token.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. scripts/create-token.py: Automated ERC20 token creation for the MMGen suite
  20. """
  21. import sys,json,re
  22. from subprocess import run,PIPE
  23. from collections import namedtuple
  24. from mmgen.cfg import Config
  25. from mmgen.util import Msg,msg,rmsg,ymsg,die
  26. ti = namedtuple('token_param_info',['default','conversion','test'])
  27. class TokenData:
  28. fields = ('decimals','supply','name','symbol','owner_addr')
  29. decimals = ti('18', int, lambda s: s.isascii() and s.isdigit() and 0 < int(s) <= 36)
  30. name = ti(None, str, lambda s: s.isascii() and s.isprintable() and len(s) < 256)
  31. supply = ti(None, int, lambda s: s.isascii() and s.isdigit() and 0 < int(s) < 2**256)
  32. symbol = ti(None, str, lambda s: s.isascii() and s.isalnum() and len(s) <= 20)
  33. owner_addr = ti(None, str, lambda s: s.isascii() and s.isalnum() and len(s) == 40) # checked separately
  34. token_data = TokenData()
  35. req_solc_ver_pat = '^0.8.6'
  36. opts_data = {
  37. 'text': {
  38. 'desc': 'Create an ERC20 token contract',
  39. 'usage':'[opts] <owner address>',
  40. 'options': f"""
  41. -h, --help Print this help message
  42. -o, --outdir=D Specify output directory for *.bin files
  43. -d, --decimals=D Number of decimals for the token (default: {token_data.decimals.default})
  44. -n, --name=N Token name (REQUIRED)
  45. -p, --preprocess Print the preprocessed code to stdout
  46. -t, --supply=T Total supply of the token (REQUIRED)
  47. -s, --symbol=S Token symbol (REQUIRED)
  48. -S, --stdout Output JSON data to stdout instead of files
  49. -v, --verbose Produce more verbose output
  50. -c, --check-solc-version Check the installed solc version
  51. """,
  52. 'notes': """
  53. The owner address must be in checksummed format.
  54. Use ‘mmgen-tool eth_checksummed_addr’ to create it if necessary.
  55. """
  56. }
  57. }
  58. # ERC Token Standard #20 Interface
  59. # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
  60. solidity_code_template = """
  61. pragma solidity %s;
  62. contract SafeMath {
  63. function safeAdd(uint a, uint b) public pure returns (uint c) {
  64. c = a + b;
  65. require(c >= a);
  66. }
  67. function safeSub(uint a, uint b) public pure returns (uint c) {
  68. require(b <= a);
  69. c = a - b;
  70. }
  71. function safeMul(uint a, uint b) public pure returns (uint c) {
  72. c = a * b;
  73. require(a == 0 || c / a == b);
  74. }
  75. function safeDiv(uint a, uint b) public pure returns (uint c) {
  76. require(b > 0);
  77. c = a / b;
  78. }
  79. }
  80. abstract contract ERC20Interface {
  81. function totalSupply() public virtual returns (uint);
  82. function balanceOf(address tokenOwner) public virtual returns (uint balance);
  83. function allowance(address tokenOwner, address spender) public virtual returns (uint remaining);
  84. function transfer(address to, uint tokens) public virtual returns (bool success);
  85. function approve(address spender, uint tokens) public virtual returns (bool success);
  86. function transferFrom(address from, address to, uint tokens) public virtual returns (bool success);
  87. event Transfer(address indexed from, address indexed to, uint tokens);
  88. event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
  89. }
  90. contract Owned {
  91. address public owner;
  92. address public newOwner;
  93. event OwnershipTransferred(address indexed _from, address indexed _to);
  94. constructor() public {
  95. owner = msg.sender;
  96. }
  97. modifier onlyOwner {
  98. require(msg.sender == owner);
  99. _;
  100. }
  101. function transferOwnership(address _newOwner) public onlyOwner {
  102. newOwner = _newOwner;
  103. }
  104. function acceptOwnership() public {
  105. require(msg.sender == newOwner);
  106. emit OwnershipTransferred(owner, newOwner);
  107. owner = newOwner;
  108. newOwner = address(0);
  109. }
  110. }
  111. // ----------------------------------------------------------------------------
  112. // ERC20 Token, with the addition of symbol, name and decimals and assisted
  113. // token transfers
  114. // ----------------------------------------------------------------------------
  115. contract Token is ERC20Interface, Owned, SafeMath {
  116. string public symbol;
  117. string public name;
  118. uint8 public decimals;
  119. uint public _totalSupply;
  120. mapping(address => uint) balances;
  121. mapping(address => mapping(address => uint)) allowed;
  122. constructor() public {
  123. symbol = "$symbol";
  124. name = "$name";
  125. decimals = $decimals;
  126. _totalSupply = $supply;
  127. balances[$owner_addr] = _totalSupply;
  128. emit Transfer(address(0), $owner_addr, _totalSupply);
  129. }
  130. function totalSupply() public override returns (uint) {
  131. return _totalSupply - balances[address(0)];
  132. }
  133. function balanceOf(address tokenOwner) public override returns (uint balance) {
  134. return balances[tokenOwner];
  135. }
  136. function transfer(address to, uint tokens) public override returns (bool success) {
  137. balances[msg.sender] = safeSub(balances[msg.sender], tokens);
  138. balances[to] = safeAdd(balances[to], tokens);
  139. emit Transfer(msg.sender, to, tokens);
  140. return true;
  141. }
  142. function approve(address spender, uint tokens) public override returns (bool success) {
  143. allowed[msg.sender][spender] = tokens;
  144. emit Approval(msg.sender, spender, tokens);
  145. return true;
  146. }
  147. function transferFrom(address from, address to, uint tokens) public override returns (bool success) {
  148. balances[from] = safeSub(balances[from], tokens);
  149. allowed[from][msg.sender] = safeSub(allowed[from][msg.sender], tokens);
  150. balances[to] = safeAdd(balances[to], tokens);
  151. emit Transfer(from, to, tokens);
  152. return true;
  153. }
  154. function allowance(address tokenOwner, address spender) public override returns (uint remaining) {
  155. return allowed[tokenOwner][spender];
  156. }
  157. // Owner can transfer out any accidentally sent ERC20 tokens
  158. function transferAnyERC20Token(address tokenAddress, uint tokens) public onlyOwner returns (bool success) {
  159. return ERC20Interface(tokenAddress).transfer(owner, tokens);
  160. }
  161. }
  162. """ % req_solc_ver_pat
  163. def create_src(cfg,template,token_data):
  164. def gen():
  165. for k in token_data.fields:
  166. field = getattr(token_data,k)
  167. if k == 'owner_addr':
  168. owner_addr = cfg._args[0]
  169. from mmgen.addr import is_coin_addr
  170. if not is_coin_addr( cfg._proto, owner_addr.lower() ):
  171. die(1,f'{owner_addr}: not a valid {cfg._proto.coin} coin address')
  172. val = '0x' + owner_addr
  173. else:
  174. val = (
  175. getattr(cfg,k)
  176. or getattr(field,'default',None)
  177. or die(1,f'The --{k} option must be specified')
  178. )
  179. if not field.test(val):
  180. die(1,f'{val!r}: invalid parameter for option --{k}')
  181. yield (k, field.conversion(val))
  182. from string import Template
  183. return Template(template).substitute(**dict(gen()))
  184. def check_solc_version():
  185. """
  186. The output is used by other programs, so write to stdout only
  187. """
  188. try:
  189. cp = run(['solc','--version'],check=True,stdout=PIPE)
  190. except:
  191. msg('solc missing or could not be executed') # this must go to stderr
  192. return False
  193. if cp.returncode != 0:
  194. Msg('solc exited with error')
  195. return False
  196. line = cp.stdout.decode().splitlines()[1]
  197. version_str = re.sub(r'Version:\s*','',line)
  198. m = re.match(r'(\d+)\.(\d+)\.(\d+)',version_str)
  199. if not m:
  200. Msg(f'Unrecognized solc version string: {version_str}')
  201. return False
  202. from semantic_version import Version,NpmSpec
  203. version = Version('{}.{}.{}'.format(*m.groups()))
  204. if version in NpmSpec(req_solc_ver_pat):
  205. Msg(str(version))
  206. return True
  207. else:
  208. Msg(f'solc version ({version_str}) does not match requirement ({req_solc_ver_pat})')
  209. return False
  210. def compile_code(cfg,code):
  211. cmd = ['solc','--optimize','--bin','--overwrite']
  212. if not cfg.stdout:
  213. cmd += ['--output-dir', cfg.outdir or '.']
  214. cmd += ['-']
  215. msg(f"Executing: {' '.join(cmd)}")
  216. cp = run(cmd,input=code.encode(),stdout=PIPE,stderr=PIPE)
  217. out = cp.stdout.decode().replace('\r','')
  218. err = cp.stderr.decode().replace('\r','').strip()
  219. if cp.returncode != 0:
  220. rmsg('Solidity compiler produced the following error:')
  221. msg(err)
  222. die(4,f'Solidity compiler exited with error (return val: {cp.returncode})')
  223. if err:
  224. ymsg('Solidity compiler produced the following warning:')
  225. msg(err)
  226. if cfg.stdout:
  227. o = out.split('\n')
  228. return {k:o[i+2] for k in ('SafeMath','Owned','Token') for i in range(len(o)) if k in o[i]}
  229. else:
  230. cfg._util.vmsg(out)
  231. if __name__ == '__main__':
  232. cfg = Config(opts_data=opts_data)
  233. if cfg.check_solc_version:
  234. sys.exit(0 if check_solc_version() else 1)
  235. if not cfg._proto.coin in ('ETH','ETC'):
  236. die(1,'--coin option must be ETH or ETC')
  237. if not len(cfg._args) == 1:
  238. cfg._opts.usage()
  239. code = create_src( cfg, solidity_code_template, token_data )
  240. if cfg.preprocess:
  241. Msg(code)
  242. sys.exit(0)
  243. out = compile_code( cfg, code )
  244. if cfg.stdout:
  245. print(json.dumps(out))
  246. msg('Contract successfully compiled')