The Smart Contract Weakness Classification and Test Cases (SWC) Registry is a set of Web3 vulnerabilities to avoid when writing smart contracts. It may seem daunting to understand every issue so I’ll do my best to demystify every issue and explain each vulnerability with real-world examples. This write-up is intended for those who are just starting out in solidity development and smart contract auditing.
SWC-112: Delegatecall to Untrusted Callee
Delegatecall is a huge subject and it’s hard to cover every aspect of this special call function, so what I will do instead is explain this concept in general terms by giving some relevant analogies. If you want further resources to deepen your understanding of delegatecall, I have linked some really useful resources under the “more reading” section below. In this write-up, I will be addressing these bullet points.
What is delegatecall?
What’s the difference between call and delegatecall?
What is the purpose of delegatecall?
Vulnerabilities of delegatecall
More Reading
Summary
References
What is delegatecall?
Imagine you're a teacher who wants to delegate a task to another teacher while still maintaining your authority over the class. You give the other teacher your lesson plan and tell them to teach it to your students. When the other teacher teaches the lesson, they follow your instructions exactly and use the same materials and resources that you would use.
Now, let's say that during the lesson, a student brings in a dollar to pay for a school fundraiser. The other teacher collects the dollar and gives it to you at the end of the lesson. Even though the other teacher collected the money, you are still the one who receives it and can decide how to use it.
Delegatecall literally means delegating someone else to do a task for you. The material and whatever earnings will be yours, but you will not be the one executing the task.
In more specific terms, when you use delegatecall to execute the function in the called contract, the called contract executes the function using the calling contract address, msg.value and msg.sender. This means that any changes to the contract, ie any ether sent with the transaction, or any message sender information will be preserved and returned to the calling contract. For example, if contract A uses delegatecall to execute a function in contract B, the address(this), msg.value and msg.sender will be contract A instead of contract B. Also, the state variable of the contract being called does not change. Only the state variable of the contract executing the delegate call changes. Delegatecall preserves the context of calling the contract just like how you had control over the money and the teaching materials although another teacher taught it and held the money.
What’s the difference between call and delegatecall?
A simple call does not preserve the context of the calling contract whereas delegatecall does. Let’s look at another analogy.
You're a CEO of a company who needs to delegate a task to another department. You could either give them the instructions and resources they need to complete the task, or you could give them a budget and let them use their own resources to complete the task.
Using delegatecall is like giving the other department the instructions and resources they need to complete the task while maintaining control over the process. The other department simply follows your instructions exactly and uses the same resources that you would use if you were completing the task yourself. This way, you can be sure that the task is completed correctly and in line with your expectations.
On the other hand, using call is like giving the other department a budget and letting them use their own resources to complete the task. They may use different resources or methods than you would use, and they may not follow your instructions exactly. As a result, the outcome of the task may be different than you intended or expected.
In coding terms, Using call creates a new execution context for the called contract, which does not have access to the state or context of the calling contract.
If you’re still unsure about the difference between call and delegatecall, below is an illustration extracted from Smart Contract Programmer on Youtube.
Normal Call:
Alice calls inc() in Contract A and passes in 1 ether
inc() in Contract A calls inc() in Contract B, which increments x by 1
In contract A, the msg.sender is Alice, the msg.value is 1 ether and x = 0
In contract B, the msg.sender is contract A, the msg.value is 0 ether and x = 1.
Delegatecall:
Alice calls inc() in Contract A and passes in 1 ether
inc() in Contract A delegatecalls inc() in Contract B
In contract A, the msg.sender is Alice, the msg.value is 1 ether, x = 1
In contract B, the msg.sender is also Alice, the msg.value is also 1 ether, but the state variable x is 0
As you can see, the msg.sender and msg.value does not change, but the state variable of contract A changes in accordance with what is called in contract B. Contract B’s state variable does not change
Now that we know the difference between delegatecall and call, what’s the purpose of using delegatecall?
What is the purpose of delegatecall?
Analogy time!
Imagine you are a restaurant owner who wants to offer a new dish on your menu. You could either hire a new chef to prepare the dish, or you could ask your existing chef to learn how to prepare the dish.
Using delegatecall is like asking your existing chef to learn how to prepare the new dish. In the context of smart contracts, this means that you can reuse existing code to add new functionality to your contract. In other words, instead of creating a new contract with the new functionality, you can create a separate contract that implements the new functionality, and then use delegatecall to execute the functions of that contract from your existing contract.
This approach has several advantages. First, it allows you to reuse existing code, which can save time and reduce the risk of bugs. Second, it allows you to add new functionality to your contract without having to modify the existing contract code. This can be particularly useful if your contract has already been deployed on the blockchain, as modifying the code of an already-deployed contract can be difficult and expensive. Finally, using delegatecall can help you to modularize your contract code, which can make it easier to read, test, and maintain.
An important usage for delegatecalls is the use of proxy. A proxy contract is a contract that acts as an intermediary between a client contract and a target contract. When a client contract sends a message to a proxy contract, the proxy contract forwards the message to the target contract. The target contract then executes the message and returns a response to the proxy contract, which forwards the response to the client contract.
One common type of proxy contract is an upgradeable proxy. An upgradeable proxy is a proxy contract that allows the target contract to be upgraded without modifying the proxy contract code. To achieve this, an upgradeable proxy typically uses delegatecall to execute the functions of the target contract. When the target contract is upgraded, the proxy contract can be updated to point to the new target contract address, without having to modify the proxy contract code. I won’t be going so much into proxies but I have listed some additional readings such as the EIP-1967, EIP-2535, and OpenZeppelin’s proxies under the “More Reading” section below.
Vulnerabilities of delegatecall
Remember the first analogy about the teacher? What if, after you have given the teacher your materials, she decides to misuse the materials? Also, when she got the money from the student, she decides to pocket it herself? Since delegatecall gives the delegatee important permissions, only delegatecall to contracts that you really trust. Also, the storage layout must be the same for the contract calling delegatecall and the contract getting called.
More Reading
Summary
delegatecall preserves context (caller, msg.value, address(this))
Same storage layout for both callee and caller contracts.
delegatecall only to trusted contracts
References
https://swcregistry.io/docs/SWC-112
https://solidity-by-example.org/hacks/delegatecall/
Thank you for reading! If you have any questions or clarifications, do reach out to @cryptostaker22 on Twitter or cryptostaker#1621 on Discord. Have a great week ahead!