Stacks is a Bitcoin Layer 2 for smart contracts; it enables smart contracts and decentralized applications to use Bitcoin as an asset and settle transactions on the Bitcoin blockchain. In the upcoming release of Stacks, Stacks will be secured by the entire hash power of Bitcoin, giving it Bitcoin finality. To facilitate this, Stacks employs its native smart contract language called Clarity. Unlike others contract languages, Clarity in smart contracts is decidable, allowing developers to predict contract behavior without execution, thereby enhancing security and reducing unforeseen behaviors.
Clarity smart contracts have a potential blind spot: misuse of the `
tx-sender` variable during authentication. This oversight mirrors vulnerabilities like those in OWASP’s threat list and SWC-115 from Solidity.
In Clarity, `
tx-sender` identifies the initiating principal (an address in Stacks):
While useful for verifying a function caller, it can, if misused, be an entry point for malicious contracts.
The Vulnerability Unpacked
Imagine a token contract using `
tx-sender` to grant transaction permissions. If a malicious contract can trick a user into initiating a function, it can call the original contract’s `
transfer` function. Why? Because `
tx-sender` doesn’t change, making it seem like the original user is authorizing transfers.
Take the following token contract as example:
(define-fungible-token vulnerable-token u100000) (define-constant err-not-token-owner (err u4001)) (define-public (transfer (amount uint) (sender principal) (recipient principal)) (begin (asserts! (is-eq tx-sender sender) err-not-token-owner) (try! (ft-transfer? vulnerable-token amount sender recipient)) (ok true)))
Here’s the exploit:
- Set up a rogue contract targeting the `
- Lure a user into activating a function within this contract — using phishing or social engineering.
- The rogue contract hits the `
transfer` function in the token contract, moving tokens due to the static `tx-sender`.
This rogue contract would look like this:
(define-constant TARGET '<SOME_PRINCIPAL>.vulnerable-token) (define-constant OWNER tx-sender) (define-public (airdrop) (contract-call? TARGET transfer u10000 tx-sender OWNER))
Several measures can plug this security hole:
contract-caller` for Stronger Authentication:
contract-caller` in Clarity points to the account or contract making the call. It’s a tighter security measure than `
With this countermeasure implemented in the vulnerable token, the attack previously described would not work.
Whitelist Trusted Contracts
When an intermediary contract is required, create a list of trusted contracts. Cross-check `
contract-caller` against this list to weed out the untrusted ones.
With Stacks 2.0, users can set conditions for contract calls. If a post-call check fails, the transaction aborts. However, this works only for token transfers, not for other phishing opportunities (e.g., if the vulnerable contract would include a function that changes the contract owner authenticating using tx-sender, then the post-condition countermeasure will not work since no token is being transferred).
The updated token contract code:
(define-fungible-token vulnerable-token u100000) (define-constant err-not-token-owner (err u4001)) (define-map trusted-contracts principal bool) (define-public (transfer (amount uint) (sender principal) (recipient principal)) (let ((is-trusted (default-to false (map-get? trusted-contracts contract-caller)))) (asserts! (or (is-eq contract-caller sender) (and is-trusted (is-eq tx-sender sender))) err-not-token-owner) (try! (ft-transfer? vulnerable-token amount sender recipient)) (ok true))) (define-public (add-trusted-contract (contract principal)) (begin (map-set trusted-contracts contract true) (ok true)))
tx-sender` can expose Clarity smart contracts to risks. But with a grasp of the problem and the right tools at hand, developers can create secure, efficient blockchain solutions.