一個簡單的以太坊合約讓imtoken支持多簽
熟悉比特幣和以太坊的人應該都知道,在比特幣中有2種類型的地址,1開頭的是P2PKH,就是個人地址,3開頭的是P2SH,一般是一個多簽地址。所以在原生上比特幣就支持多簽。多簽的一個優勢就是可以多方對一筆付款達成共識,才能支付成功。比如3個人合夥開公司,他們的對外付款是比特幣,為了防止管理財務的人作惡,於是他們可以創建2/3多簽的地址,每個人持有一個私鑰,對於每一筆付款,必須任意2個人都簽名了才能支付出去。
比特幣上的這個多簽地址在以太坊上是沒有原生支持的!以太坊最大的優點是支持圖靈完備的智能合約,所以多簽功能需要靠智能合約來實現。
為了簡化代碼,我們的需求是這樣的:創建一個AB兩個用戶創建2/2的多簽合約,該合約支持指定的ERC20 Token的支付。當某需要對外付款時,A用戶調用合約,發起對C的轉賬n個Token,B用戶也必須調用合約,發起對C的轉賬n個Token,只有A和B都調用了合約後,合約才會真的付款。其他用戶發起轉賬無效。
根據以上需求,我改了一款極其簡單的多簽合約。代碼如下:
pragma solidity ^0.4.24; interface Token { function balanceOf(address _owner) public view returns (uint256 ); function transfer(address _to, uint256 _value) public ; } contract MultiSig { address private addrA; address private addrB; address private addrToken; struct Permit { bool addrAYes; bool addrBYes; } mapping (address=> mapping (uint => Permit)) private permits; event Transfer(address indexed from, address indexed to, uint256 value); constructor(address a, address b, address tokenAddress) public{ addrA = a; addrB = b; addrToken = tokenAddress; } functiongetAddrs() public view returns(address, address,address) { return (addrA, addrB,addrToken); } function transferTo(address to, uint amount) public{ Token token = Token(addrToken); require(token.balanceOf(this) >= amount); if (msg.sender == addrA) { permits[to][amount].addrAYes = true; } else if (msg.sender == addrB) { permits[to][amount].addrBYes = true; } else { require(false); } if (permits[to][amount].addrAYes == true && permits[to][amount].addrBYes == true) { token.transfer(to, amount); permits[to][amount].addrAYes = false; permits[to][amount].addrBYes = false; } emit Transfer(msg.sender, to, amount); } }
以上代碼十分簡陋,功能十分有限,而且需要在etherscan或者remix上調用,對用戶來說十分不友好,於是我想到了可以按ERC20標準接口對這個多簽合約進行改造。改造後的合約看起來像是一個Token,但是本質上是一個多簽地址。A B用戶都可以使用imtoken或者KCash之類的支持ERC20的錢包APP進行多簽,而不需要任何復雜的技能。所以我改寫後的多簽合約如下:
pragma solidity ^0.4.24; interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function totalSupply() external view returns (uint256); function balanceOf(address who) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } contract MultiSig is IERC20 { address private addrA; address private addrB; address private addrToken; struct Permit { bool addrAYes; bool addrBYes; } mapping (address => mapping (uint => Permit)) private permits; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); uint public totalSupply = 10*10**26; uint8 constant public decimals = 18; string constant public name = "MutiSigPTN"; string constant public symbol = "MPTN"; function approve(address spender, uint256 value) external returns (bool){ return false; } function transferFrom(address from, address to, uint256 value) external returns (bool){ return false; } function totalSupply() external view returns (uint256){ IERC20 token = IERC20(addrToken); return token.totalSupply(); } function allowance(address owner, address spender) external view returns (uint256){ return 0; } constructor(address a, address b, address tokenAddress) public{ addrA = a; addrB = b; addrToken = tokenAddress; } function getAddrs() public view returns(address, address,address) { return (addrA, addrB,addrToken); } function transfer(address to, uint amount) public returns (bool){ IERC20 token = IERC20(addrToken); require(token.balanceOf(this) >= amount); if (msg.sender == addrA) { permits[to][amount].addrAYes = true; } else if (msg.sender == addrB) { permits[to][amount].addrBYes = true; } else { require(false); } if (permits[to][amount].addrAYes == true && permits[to][amount].addrBYes == true) { token.transfer(to, amount); permits[to][amount].addrAYes = false; permits[to][amount].addrBYes = false; } emit Transfer(msg.sender, to, amount); return true; } function balanceOf(address _owner) public view returns (uint) { IERC20 token = IERC20(addrToken); if (_owner==addrA || _owner==addrB){ return token.balanceOf(this); } return 0; } }
這裏我需要特別指出的是decimals這個屬性必須與所支持的ERC20 Token一致,這樣錢包才會算出正確的轉賬金額。另外imtoken的緩存刷新比較慢,並不是部署了合約後馬上就能搜索到的。以上代碼都實測沒問題。
一個簡單的以太坊合約讓imtoken支持多簽