![]() |
|
[Tut] DelegateCall or Storage Collision Attack on Smart Contracts - Printable Version +- Sick Gaming (https://sickgaming.net) +-- Forum: Programming (https://sickgaming.net/forum-76.html) +--- Forum: Python (https://sickgaming.net/forum-83.html) +--- Thread: [Tut] DelegateCall or Storage Collision Attack on Smart Contracts (/thread-99479.html) |
[Tut] DelegateCall or Storage Collision Attack on Smart Contracts - xSicKxBot - 05-30-2022 DelegateCall or Storage Collision Attack on Smart Contracts 5/5 – (1 vote) The post is part 6 and continues our Smart Contract Security Series:
The Code: DelegateCallBefore you can grasp this exploit, you must first understand how Solidity saves state variables as explained here. We start with the differences between Code: callCode: delegatecallCode: delegatecallLet’s start the journey! Call VS DelegateCall Solidity supports two low-level interfaces for interaction or sending messages to the contract functions. These interfaces operate on addresses rather than contract instances (using Code: thisCall It allows you to call the code of the callee contract from the caller with the storage context of the callee. In order to understand this confusing sentence, let’s consider two contracts Code: ACode: CallA
Code: // Callee
contract A
{ uint256 public x; function foo(uint256 _x) public { x = _x; }
} // Caller
contract CallA
{ uint256 public x; function callfoo(address _a) public { (bool success,) = _a.call(abi.encodeWithSignature("foo(uint256)", 15)); require(success, "Call was not successful"); }
}To test, deploy the contracts on Remix, and when you execute the caller ( Code: CallA -> callfooCode: foo()Code: xCode: callee(A ->x)Code: 15 Note: It is also possible to send Ether and gas as part of the call using Code: valueCode: gasThe above scenario is described in the figure as shown. Fig: call flowDelegateCall It allows you to call the code of the callee contract from the caller with the storage context of the caller. As previously mentioned, let’s consider two contracts Code: ACode: DelegateCallA
Code: contract A
{ uint256 public x; function foo(uint256 _x) public { x = _x; }
} contract DelegateCallA
{ uint256 public x; function callfoo(address _a) public { (bool success,) = _a.delegatecall(abi.encodeWithSignature("foo(uint256)", 15)); require(success, "Delegate Call was not successful"); }
}To test, deploy the contracts on Remix, and when you execute the caller ( Code: DelegateCallA -> ‘callfoo’Code: foo()Code: ‘x‘Code: callee(A ->x)Code: xCode: DelegateCallA -> xCode: 15Equipped with the above examples, it is evident that the Code: delegatecallCode: callA picture speaks a thousand words. The above scenario is in the below figure. Fig: Code: delegatecallOne use case of Code: callCode: delegatecallExploit with delegatecall The most widely adopted technique to upgrade contracts is utilizing a proxy contract. A proxy interposes the actual logical contract and the dapp interface. To update the logical contract with a new version (say V2), only the new deployed address of the logical contract is passed to the proxy. This helps achieve minimal or no changes in the dapp/web3 interface, saving a lot of development time. Fig: Contract upgrade with proxyLet us write a quick and short proxy, and a logical contract (say V1). For the same, create a file Code: DelegateCall.solCode: contract Proxy
{ uint256 public x; address public owner; address public logicalAddr; constructor(address _Addr) { logicalAddr = _Addr; owner = msg.sender; } function upgrade(address _newAddr) public { logicalAddr = _newAddr; } // To call any function of the logical contract fallback() external payable { (bool success, ) = logicalAddr.delegatecall(msg.data); require(success , " Error calling logical contract"); }
}V1, This represents version 1 of the logical contract. Code: contract V1
{ uint256 public x; // abi.encodedWithSignature("increment_X()") = 0xeaf2926e function increment_X() public { x += 1; }
}Compile, deploy and run the contracts in Remix with the constructor param in proxy as the address of the V1 contract. You can observe that, when the Code: abi.encodedWithSignature("increment_X()"))Code: calldataCode: fallback()Code: increment_X()Abi encoding is calculated using the tool, Code: $ npm install web3-eth-abiand then Code: const Web3EthAbi = require('web3-eth-abi'); > Web3EthAbi.encodeFunctionSignature("increment_X()") '0xeaf2926e'As discussed above in Code: delegatecallCode: xSo far, this is all good. At some point in the future, it is decided to upgrade the V1 contract with new functionality, let’s call it V2. Create a new contract V2 Code: contract V2
{ uint256 public x; uint256 public y; function increment_X() public { x += 1; } // abi.encodedWithSignature("set_Y(uint256)", 10) //0x1675b4f5000000000000000000000000000000000000000000000000000000000000000a function set_Y(uint256 _y) public { y = _y; }
}Compile and deploy V2. Pass the address of V2, to Code: upgrade()When Code: abi.encodedWithSignature("set_Y(uint256)", 10))Code: calldataCode: proxyCode: increment_Y()The value of Code: yAs there is no Code: yCode: ProxyCode: 0x000000000000000000000000000000000000000AWith the owner address changed, the attacker is now in complete control of all the contracts. How to Prevent the Attack The Code: delegatecallFor example, possible solutions to the above problem can be
Outro In this tutorial, we saw how Code: delegatecallWhile using Code: delegatecallCode: delegatecallEven though there are certain problems associated with Code: delegatecallTo conclude, use Code: delegatecall
|