External Contract Referencing

One of the benefits of the Ethereum “world computer” is the ability to reuse code and interact with contracts already deployed on the network. As a result, a large number of contracts reference external contracts, usually via external message calls. These external message calls can mask malicious actors’ intentions in some nonobvious ways, which we’ll now examine.

The Vulnerability

In Solidity, any address can be cast to a contract, regardless of whether the code at the address represents the contract type being cast. This can cause problems, especially when the author of the contract is trying to hide malicious code. Let’s illustrate this with an example.

Consider a piece of code like Rot13Encryption.sol, which rudimentarily implements the ROT13 cipher.

Example 8. Rot13Encryption.sol

  1. // encryption contract
  2. contract Rot13Encryption {
  3. event Result(string convertedString);
  4. // rot13-encrypt a string
  5. function rot13Encrypt (string text) public {
  6. uint256 length = bytes(text).length;
  7. for (var i = 0; i < length; i++) {
  8. byte char = bytes(text)[i];
  9. // inline assembly to modify the string
  10. assembly {
  11. // get the first byte
  12. char := byte(0,char)
  13. // if the character is in [n,z], i.e. wrapping
  14. if and(gt(char,0x6D), lt(char,0x7B))
  15. // subtract from the ASCII number 'a',
  16. // the difference between character <char> and 'z'
  17. { char:= sub(0x60, sub(0x7A,char)) }
  18. if iszero(eq(char, 0x20)) // ignore spaces
  19. // add 13 to char
  20. {mstore8(add(add(text,0x20), mul(i,1)), add(char,13))}
  21. }
  22. }
  23. emit Result(text);
  24. }
  25. // rot13-decrypt a string
  26. function rot13Decrypt (string text) public {
  27. uint256 length = bytes(text).length;
  28. for (var i = 0; i < length; i++) {
  29. byte char = bytes(text)[i];
  30. assembly {
  31. char := byte(0,char)
  32. if and(gt(char,0x60), lt(char,0x6E))
  33. { char:= add(0x7B, sub(char,0x61)) }
  34. if iszero(eq(char, 0x20))
  35. {mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
  36. }
  37. }
  38. emit Result(text);
  39. }
  40. }

This code simply takes a string (letters a–z, without validation) and encrypts it by shifting each character 13 places to the right (wrapping around z); i.e., a shifts to n and x shifts to k. The assembly in the preceding contract does not need to be understood to appreciate the issue being discussed, so readers unfamiliar with assembly can safely ignore it.

Now consider the following contract, which uses this code for its encryption:

  1. import "Rot13Encryption.sol";
  2. // encrypt your top-secret info
  3. contract EncryptionContract {
  4. // library for encryption
  5. Rot13Encryption encryptionLibrary;
  6. // constructor - initialize the library
  7. constructor(Rot13Encryption _encryptionLibrary) {
  8. encryptionLibrary = _encryptionLibrary;
  9. }
  10. function encryptPrivateData(string privateInfo) {
  11. // potentially do some operations here
  12. encryptionLibrary.rot13Encrypt(privateInfo);
  13. }
  14. }

The issue with this contract is that the encryptionLibrary address is not public or constant. Thus, the deployer of the contract could give an address in the constructor that points to this contract:

  1. // encryption contract
  2. contract Rot26Encryption {
  3. event Result(string convertedString);
  4. // rot13-encrypt a string
  5. function rot13Encrypt (string text) public {
  6. uint256 length = bytes(text).length;
  7. for (var i = 0; i < length; i++) {
  8. byte char = bytes(text)[i];
  9. // inline assembly to modify the string
  10. assembly {
  11. // get the first byte
  12. char := byte(0,char)
  13. // if the character is in [n,z], i.e. wrapping
  14. if and(gt(char,0x6D), lt(char,0x7B))
  15. // subtract from the ASCII number 'a',
  16. // the difference between character <char> and 'z'
  17. { char:= sub(0x60, sub(0x7A,char)) }
  18. // ignore spaces
  19. if iszero(eq(char, 0x20))
  20. // add 26 to char!
  21. {mstore8(add(add(text,0x20), mul(i,1)), add(char,26))}
  22. }
  23. }
  24. emit Result(text);
  25. }
  26. // rot13-decrypt a string
  27. function rot13Decrypt (string text) public {
  28. uint256 length = bytes(text).length;
  29. for (var i = 0; i < length; i++) {
  30. byte char = bytes(text)[i];
  31. assembly {
  32. char := byte(0,char)
  33. if and(gt(char,0x60), lt(char,0x6E))
  34. { char:= add(0x7B, sub(char,0x61)) }
  35. if iszero(eq(char, 0x20))
  36. {mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
  37. }
  38. }
  39. emit Result(text);
  40. }
  41. }

This contract implements the ROT26 cipher, which shifts each character by 26 places (i.e., does nothing). Again, there is no need to understand the assembly in this contract. More simply, the attacker could have linked the following contract to the same effect:

  1. contract Print{
  2. event Print(string text);
  3. function rot13Encrypt(string text) public {
  4. emit Print(text);
  5. }
  6. }

If the address of either of these contracts were given in the constructor, the encryptPrivateData function would simply produce an event that prints the unencrypted private data.

Although in this example a library-like contract was set in the constructor, it is often the case that a privileged user (such as an owner) can change library contract addresses. If a linked contract doesn’t contain the function being called, the fallback function will execute. For example, with the line encryptionLibrary.rot13​Encrypt(), if the contract specified by encryptionLibrary was:

  1. contract Blank {
  2. event Print(string text);
  3. function () {
  4. emit Print("Here");
  5. // put malicious code here and it will run
  6. }
  7. }

then an event with the text Here would be emitted. Thus, if users can alter contract libraries, they can in principle get other users to unknowingly run arbitrary code.

Warning

The contracts represented here are for demonstrative purposes only and do not represent proper encryption. They should not be used for encryption.

Preventative Techniques

As demonstrated previously, safe contracts can (in some cases) be deployed in such a way that they behave maliciously. An auditor could publicly verify a contract and have its owner deploy it in a malicious way, resulting in a publicly audited contract that has vulnerabilities or malicious intent.

There are a number of techniques that prevent these scenarios.

One technique is to use the new keyword to create contracts. In the preceding example, the constructor could be written as:

  1. constructor() {
  2. encryptionLibrary = new Rot13Encryption();
  3. }

This way an instance of the referenced contract is created at deployment time, and the deployer cannot replace the Rot13Encryption contract without changing it.

Another solution is to hardcode external contract addresses.

In general, code that calls external contracts should always be audited carefully. As a developer, when defining external contracts, it can be a good idea to make the contract addresses public (which is not the case in the honey-pot example in the following section) to allow users to easily examine code referenced by the contract. Conversely, if a contract has a private variable contract address it can be a sign of someone behaving maliciously (as shown in the real-world example). If a user can change a contract address that is used to call external functions, it can be important (in a decentralized system context) to implement a time-lock and/or voting mechanism to allow users to see what code is being changed, or to give participants a chance to opt in/out with the new contract address.

Real-World Example: Reentrancy Honey Pot

A number of recent honey pots have been released on the mainnet. These contracts try to outsmart Ethereum hackers who try to exploit the contracts, but who in turn end up losing ether to the contract they expect to exploit. One example employs this attack by replacing an expected contract with a malicious one in the constructor. The code can be found here:

  1. pragma solidity ^0.4.19;
  2. contract Private_Bank
  3. {
  4. mapping (address => uint) public balances;
  5. uint public MinDeposit = 1 ether;
  6. Log TransferLog;
  7. function Private_Bank(address _log)
  8. {
  9. TransferLog = Log(_log);
  10. }
  11. function Deposit()
  12. public
  13. payable
  14. {
  15. if(msg.value >= MinDeposit)
  16. {
  17. balances[msg.sender]+=msg.value;
  18. TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
  19. }
  20. }
  21. function CashOut(uint _am)
  22. {
  23. if(_am<=balances[msg.sender])
  24. {
  25. if(msg.sender.call.value(_am)())
  26. {
  27. balances[msg.sender]-=_am;
  28. TransferLog.AddMessage(msg.sender,_am,"CashOut");
  29. }
  30. }
  31. }
  32. function() external payable{}
  33. }
  34. contract Log
  35. {
  36. struct Message
  37. {
  38. address Sender;
  39. string Data;
  40. uint Val;
  41. uint Time;
  42. }
  43. Message[] public History;
  44. Message LastMsg;
  45. function AddMessage(address _adr,uint _val,string _data)
  46. public
  47. {
  48. LastMsg.Sender = _adr;
  49. LastMsg.Time = now;
  50. LastMsg.Val = _val;
  51. LastMsg.Data = _data;
  52. History.push(LastMsg);
  53. }
  54. }

This post by one reddit user explains how they lost 1 ether to this contract by trying to exploit the reentrancy bug they expected to be present in the contract.