Understanding Tx-Sender: Safeguarding Against Exploitation
Written on
Chapter 1: The Importance of Keywords in Clarity
Keywords play a pivotal role in programming languages, and Clarity, the language that underpins the secured-by-Bitcoin Stacks, is no exception. Unlike some other languages, where the occasional neglect of keywords may be inconsequential, in the realm of smart contracts, even the slightest ambiguity can create vulnerabilities. Such vulnerabilities can lead to minor bugs or, even worse, costly exploitation avenues.
Among the most frequently used keywords in Clarity are contract-caller and tx-sender, which refer to wallet addresses (principals). Alongside block-height, these keywords form a foundational part of any Clarity developer's toolkit from day one. While they may seem straightforward at first glance, a deep understanding of their nuanced implementations is essential. Both keywords possess inherent security, yet they can be exploited in various ways.
In this video titled E35: The Mintery, ExecutorDAO, Wastelander Apes, Stacks turns 1, ALEX Launches, Setzeus blog post, we delve into the intricacies of the Clarity language and discuss its security features.
Section 1.1: Understanding Tx-Sender
Today, we will focus specifically on the tx-sender keyword. Its defining characteristic—principal persistence as the transaction originator—can be exploited through phishing and hijacking techniques.
Bypassing Admin Asserts with Tx-Sender Phishing
Most contracts are structured similarly, defining constants, variables, maps, and a body of public and administrative functions. Admin functions enable critical actions, such as modifying a minting price or pausing important public functions. Typically, these public functions implement admin checks, like so:
(asserts! (is-eq tx-sender [admin-address]) (err NOT-AUTHORIZED))
Next, we will investigate how careless developers and inattentive users can allow tx-sender persistence to be exploited in order to bypass these admin checks.
Explanation of Keywords
Let's clarify the roles of the two keywords as defined in the Clarity documentation:
- Contract-Caller: Represents the principal that invoked the function.
- Tx-Sender: Represents the principal that initiated the transaction.
At first glance, the difference between "invoked the function" and "initiated the transaction" may appear negligible, but this deceptive similarity often leads developers to misuse tx-sender without fully grasping its implications. A more intuitive rephrasing could be:
- Contract-Caller: Most Recent Principal in the Transaction Chain.
- Tx-Sender: Originating Principal in the Transaction Chain.
When I reference "Transaction Chain," I am alluding to the sequence of internal or external function calls that occur when a public function is invoked, which can be thought of as transactions. For instance, in an NFT minting process using a fungible token like $MIA, the client initiates a mint transaction, but this may also involve a contract call to the $MIA token smart contract, as shown below:
(define-public (claim-example)
(begin
…
(unwrap! (contract-call? 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.miamicoin-token transfer mint-price-mia contract-caller contract-owner (some 0x00)) (err u102))
(nft-mint? example-nft (var-get last-id) tx-sender)
…
)
)
In this context, while tx-sender consistently refers to the transaction originator, the meaning of contract-caller may fluctuate, referencing the most recent transaction sender, who might not be the originator.
Exploiting Tx-Sender Persistence
Consider a scenario where Bob, an admin for an NFT project (let's name it NF3), is subject to asserts! checks in vital admin functions like updating the admin address or mint price. The following is a simplified example of an admin function for updating the admin address:
;; contract NF3.clar
(define-public (update-admin-address (new-admin principal)
(begin
;; checks that current admin (Bob) calling is tx-sender
(asserts! (is-eq tx-sender (var-get current-admin)) (err u101))
;; updates admin to new-admin param
(ok (var-set current-admin new-admin)
)
)
This function performs two essential tasks: it verifies that tx-sender is the current admin (initially Bob's) and updates the current-admin variable to the new admin (Alice).
In the video How to Pair a new Dexcom G6 Transmitter, we discuss the importance of understanding system interactions and the potential risks involved.
Now, consider if Bob were to invoke this function from an untrusted front-end and quickly skimmed through the post-conditions. A malicious actor could exploit the persistence of tx-sender to bypass the asserts! check in NF3.clar, as illustrated below:
;; contract exploit.clar
(define-public (phish-claim
;; calling new-admin() of NF3.clar from this contract first initiated by Bob
(ok (as-contract (try! (contract-call? .NF3 new-admin [hacker-address-here]))))
)
The [hacker-address-here] parameter can be manipulated to any desired principal. If Bob, or the current admin, is tricked into calling the above contract, the following occurs:
The asserts! check in NF3 verifies tx-sender, which remains the originating transaction sender. Consequently, since Bob initiated the transaction chain by falling victim to phishing, the tx-sender check will succeed, setting the new admin to Elliot, the attacker. This scenario is particularly alarming as it grants the attacker admin privileges moving forward, but the exploit logic applies to any admin function that relies on tx-sender: if the originator is ever phished, the persistence of tx-sender can be manipulated.
Prevention Strategies
For non-technical users, there are two primary strategies to minimize the risk of exploitation. First, avoid interacting with any unfamiliar front-end, especially those with disabled post-conditions or running in allow-mode. Second, always take the time to review every single post-condition before confirming a transaction—do not rush and stop reading prematurely. Malicious conditions can easily be obscured by sheer volume, so thoroughness is crucial.
For developers, the prevention strategy is straightforward: internalize the distinctions between contract-caller and tx-sender. Test the discussed scenario yourself and carefully consider how the context of tx-sender will persist while writing your next contract. Always assume that someone might create a malicious contract that exploits this persistence and take steps to mitigate that risk.