소프트웨어를 원하는대로 개발하는 것은 어렵지 않지만, 다른 사람이 아예 다른 의도로 작동시키는걸 막는 것은 어렵습니다. 솔리디티에서는 모든 스마트 컨트랙트가 공개적으로 실행되고 대부분의 소스코드를 확인할 수 있는 경우가 많습니다. 따라서 이런 보안 측면의 고려사항은 특히 더 중요합니다. 물론 보안에 얼마나 신경을 써야하는 지는 상황에 따라 다릅니다. 가령, 웹 서비스는 공격자를 포함한 대중에게 공개되어야하고, 누구나 접근할 수 있어야하며, 어떤 때에는 오픈소스로 관리되는 경우도 있습니다. 만약 웹 서비스에 중요치 않은 정보만 저장한다면 문제가 되지 않지만, 은행 계좌와 같은 정보를
관리한다면 더욱 조심할 필요가 있죠. 이 장에서는 조심해야할 문제들과 일반적인 보안관련 패턴들을 다룹니다. 하지만 이는 완벽한 해결법이 아닙니다. 즉, 스마트 컨트랙트 상에는 버그가 없더라도, 컴파일러나 플랫폼 자체에 버그가 있을 수 있다는 얘기죠. 언제나 그렇듯이, 이 문서는 오픈 소스 기반의 문서이기 때문에, 보안에 대한 문제가 생긴다면 주저없이 내용을 추가해주시기 바랍니다. 스마트 컨트랙트 상의 모든 정보는 공개적으로 보여집니다. 심지어 지역 변수 및 상태 변수가 ``private``으로 선언되었다고해도 마찬가지죠. 만약 당신이 채굴자의 부정 행위를 막고자 한다면, 난수를 생성하는 것이 어느정도 유용할 수 있습니다. (A)콘트랙트에서 (B)콘트랙트로 연결되는 어떠한 상호작용 및 Ether의 전송은 제어권을 (B)에게 넘겨주게 됩니다. 이 때문에 B의 상호작용이 끝나기 전에 다시 A를 호출할 수 있는 상황이 벌어질 수 있습니다. 예를 들어, 다음 코드는 버그를 포함하고 있습니다(요약된 코드입니다). ``send``함수 자체에서 gas의 소비량을 제어하기 때문에, 큰 문제는 되지 않지만, 그럼에도 이 코드는 보안 상의 문제를 가지고 있습니다. Ether의 전송은 항상 코드의 실행을 포함하기에, 수신자는 반복적으로 ``withdraw``를 실행할 수 있게되죠. 결과적으로 중복된 ``withdraw``함수의 실행을 통해 컨트랙트 상의 모든 Ether를 가져갈 수 있다는 의미입니다. 상황에 따라, 공격자는 아래 코드 속 ``call``을 통해 남은 gas를 모두 가져올 수 있을지도 모릅니다. pragma solidity ^0.4.0; // 버그가 포함된 코드입니다. 사용하지 마세요! contract Fund { /// 컨트랙트의 Ether 정보 mapping mapping(address => uint) shares; /// 지분을 인출하는 함수 function withdraw() public { if (msg.sender.call.value(shares[msg.sender])()) shares[msg.sender] = 0; } } 재진입 공격을 막기 위해서는 아래와 같이 Checks-Effects-Interactions 패턴을 사용할 수 있습니다. pragma solidity ^0.4.11; contract Fund { /// 컨트랙트의 Ether 정보 mapping mapping(address => uint) shares; /// 지분을 인출하는 함수 function withdraw() public { var share = shares[msg.sender]; shares[msg.sender] = 0; msg.sender.transfer(share); } } 재진입 공격은 Ether 전송에서 뿐만 아니라 함수를 호출하는 어떤 상황에서도 수행될 수 있습니다. 나아가, 여러분은 하나의 계정에 많은 컨트랙트를 가질 수도 있을 텐데요, 이 때, 하나의 컨트랙트가 다른 컨트랙트를 호출할 수 있다는 것도 알아둬야합니다. 가스 제한 및 루프¶Loops that do not have a fixed number of iterations, for example, loops that depend on storage values, have to be used carefully: Due to the block gas limit, transactions can only consume a certain amount of gas. Either explicitly or just due to normal operation, the number of iterations in a loop can grow beyond the block gas limit which can cause the complete
contract to be stalled at a certain point. This may not apply to Ether 보내고 받기¶
콜스택 깊이¶External function calls can fail any time because they exceed the maximum call stack of 1024. In such situations, Solidity throws an exception. Malicious actors might be able to force the call stack to a high value before they interact with your contract. Note that tx.origin¶Never use tx.origin for authorization. Let's say you have a wallet contract like this: pragma solidity ^0.4.11; // THIS CONTRACT CONTAINS A BUG - DO NOT USE contract TxUserWallet { address owner; function TxUserWallet() public { owner = msg.sender; } function transferTo(address dest, uint amount) public { require(tx.origin == owner); dest.transfer(amount); } } Now someone tricks you into sending ether to the address of this attack wallet: pragma solidity ^0.4.11; interface TxUserWallet { function transferTo(address dest, uint amount) public; } contract TxAttackWallet { address owner; function TxAttackWallet() public { owner = msg.sender; } function() public { TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance); } } If your wallet had checked Minor Details¶
Recommendations¶Restrict the Amount of Ether¶Restrict the amount of Ether (or other tokens) that can be stored in a smart contract. If your source code, the compiler or the platform has a bug, these funds may be lost. If you want to limit your loss, limit the amount of Ether. Keep it Small and Modular¶Keep your contracts small and easily understandable. Single out unrelated functionality in other contracts or into libraries. General recommendations about source code quality of course apply: Limit the amount of local variables, the length of functions and so on. Document your functions so that others can see what your intention was and whether it is different than what the code does. Use the Checks-Effects-Interactions Pattern¶Most functions will first perform some checks (who called the function, are the arguments in range, did they send enough Ether, does the person have tokens, etc.). These checks should be done first. As the second step, if all checks passed, effects to the state variables of the current contract should be made. Interaction with other contracts should be the very last step in any function. Early contracts delayed some effects and waited for external function calls to return in a non-error state. This is often a serious mistake because of the re-entrancy problem explained above. Note that, also, calls to known contracts might in turn cause calls to unknown contracts, so it is probably better to just always apply this pattern. Include a Fail-Safe Mode¶While making your system fully decentralised will remove any intermediary, it might be a good idea, especially for new code, to include some kind of fail-safe mechanism: You can add a function in your smart contract that performs some self-checks like "Has any Ether leaked?", "Is the sum of the tokens equal to the balance of the contract?" or similar things. Keep in mind that you cannot use too much gas for that, so help through off-chain computations might be needed there. If the self-check fails, the contract automatically switches into some kind of "failsafe" mode, which, for example, disables most of the features, hands over control to a fixed and trusted third party or just converts the contract into a simple "give me back my money" contract. Formal Verification¶Using formal verification, it is possible to perform an automated mathematical proof that your source code fulfills a certain formal specification. The specification is still formal (just as the source code), but usually much simpler. Note that formal verification itself can only help you understand the difference between what you did (the specification) and how you did it (the actual implementation). You still need to check whether the specification is what you wanted and that you did not miss any unintended effects of it. |