| marp | true |
|---|---|
| title | Zeus |
| theme | default |
| paginate | true |
| author | Christian Bale |
| math | mathjax |
| style | .columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } .big-title { font-size: 2.63rem; } .lll-title { font-size: 1.5rem; } |
⚡ ⚡ ⚡
Tommy and Idan
- Smart contracts are programs that run on the blockchain
- They are written in high-level languages such as Solidity
- Faithful execution of a smart contract is enforced by the blockchain’s consensus protocol
- Correctness and fairness of the smart contracts is not enforced by the blockchain, and should be verified by the developer
- Correctness means the code is accurate and complete, producing intended results without errors and bugs
- Fairness means the code adheres to the agreed upon higher-level business logic for interaction The code shouldn't be biased towards any party, and shouldn't allow any party to cheat
while (Balance > (depositors[index].Amount * 115/100) && index<Total_Investors) {
if(depositors[index].Amount!=0)) {
payment = depositors[index].Amount * 115/100;
depositors[index].EtherAddress.send(payment);
Balance -= payment;
Total_Paid_Out += payment;
depositors[index].Amount=0; // Remove investor
} break;
}The contract offers a 15% payout to any investor. Sadly, the contract has both fairness and correctness issues.
while (Balance > (depositors[index].Amount * 115/100) && index<Total_Investors) {
if(depositors[index].Amount!=0)) {
payment = depositors[index].Amount * 115/100;
depositors[index].EtherAddress.send(payment);
Balance -= payment; // --------------------------
Total_Paid_Out += payment; // POTENTIAL OVERFLOW! 😱😱😱
depositors[index].Amount=0; // --------------------------
} break;
}Correctness issue: The contract has a potential overflow in the Total_Paid_Out variable.
while (Balance > (depositors[index].Amount * 115/100) && index<Total_Investors) {
if(depositors[index].Amount!=0)) {
payment = depositors[index].Amount * 115/100;
depositors[index].EtherAddress.send(payment);
Balance -= payment;
Total_Paid_Out += payment;
depositors[index].Amount=0;
} break;
}Fairness issue (1): index is never incremented within the loop, and so the payout is made to just one investor.
while (Balance > (depositors[index].Amount * 115/100) && index<Total_Investors) {
if(depositors[index].Amount!=0)) {
payment = depositors[index].Amount * 115/100;
depositors[index].EtherAddress.send(payment);
Balance -= payment;
Total_Paid_Out += payment;
depositors[index].Amount=0;
} break; // <------------------------------------
}Fairness issue (2): The break statement is inside the while statement, and so the loop will always break after the first iteration.
Meaning, only the first investor will get paid. (Prob. the owner)
contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
// ...
}contract AttackerContract {
function () {
Wallet wallet;
wallet.withdrawBalance();
}
}contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
userBalances[msg.sender] = 0; // Mitigated by swapping the lines
msg.sender.call(userBalances[msg.sender]);
}
}
// ...
}contract AttackerContract {
function () {
Wallet wallet;
wallet.withdrawBalance();
}
}- Solidity allows only
$2300$ gas upon a send call - Computation-heavy fallback function at the receiving contract will cause the invoking send to fail
- Contracts not handling failed send calls correctly may result in the loss of Ether
if (gameHasEnded && !prizePaidOut) {
winner.send(1000); // Send a prize to the winner
prizePaidOut = True;
}The send call may fail, but prizePaidOut is set to True regardless.
Meaning the prize will never be paid out. 😢
- Best practices suggest executing a
throwupon a failedsend, in order to revert the transaction - However, this may put contracts in risk
for (uint i=0; i < investors.length; i++) {
if (investors[i].invested == min investment) {
payout = investors[i].payout;
if (!(investors[i].address.send(payout)))
throw;
investors[i] = newInvestor;
}
}- A DAO that pays dividends to its smallest investor when a new investor offers more money, and the smallest is replaced
- A wallet with a fallback function that takes more than
$2300$ gas to run can invest enough to become the smallest investor - No new investors will be able to join the DAO
uint payout = balance/participants.length;
for (var i = 0; i < participants.length; i++)
participants[i].send(payout);-
iis of typeuint8, and so it will overflow after$255$ iterations - Attacker can fill up the first
$255$ slots in the array, and gain payouts at the expense of other investors
- Contract writers can utilize transaction state variables, such as
tx.originandtx.gasprice, for managing control flow within a smart contract tx.gaspriceis fixed and is published upfront - cannot be exploited 😄tx.originallows a contract to check the address that originally initiated the call chain
contract UserWallet {
function transfer(address dest, uint amount) {
if (tx.origin != owner)
throw;
dest.send(amount);
}
}contract AttackWallet {
function() {
UserWallet w = UserWallet(userWalletAddr);
w.transfer(thiefStorageAddr, msg.sender.balance);
}
}contract UserWallet {
function transfer(address dest, uint amount) {
if (msg.sender != owner) // FIXED!
throw;
dest.send(amount);
}
}tx.originis the address of the original initiator of the call chainmsg.senderis the address of the caller of the current function
-
Access to sensitive resources and APIs must be guarded, for instance:
-
selfdestruct:- Kill a contract and send its balance to a given address
- Should be preceded by a check that only the owner of the contract is allowed to kill it
- Several contracts did not have this check
while (balance > persons[payoutCursor_Id_].deposit / 100 * 115) {
payout = persons[payoutCursor_Id_].deposit / 100 * 115;
persons[payoutCursor_Id].EtherAddress.send(payout);
balance -= payout;
payoutCursor_Id_ ++;
}- Two similar variables,
payoutCursor_IdandpayoutCursor_Id_ - The deposits of all investors go to the 0th participant, possibly the person who created the contract
function placeBid(uint auctionId){
Auction a = auctions[auctionId];
if (a.currentBid >= msg.value)
throw;
uint bidIdx = a.bids.length++;
Bid b = a.bids[bidIdx];
b.bidder = msg.sender;
b.amount = msg.value;
// ...
BidPlaced(auctionId, b.bidder, b.amount);
return true;
}- The contract does not disclose whether it is "with reserve" or not
- The seller can participate in the auction and artificially bid up the price
- The seller can withdraw the property from the auction before it is sold
- Takes as input a smart contract and a policy against which the smart contract must be verified
- Performs static analysis atop the smart contract code
- Inserts the policy predicates as asserts
- Converts the smart contract embedded with policy assertions to LLVM bitcode
- Invokes its verifier to determine assertion violations
- Abstract language that captures relevant constructs of Solidity programs
- A program consists of a sequence of contract declarations.
- Each contract is abstractly viewed as a sequence of one or more method definitions
- A program consists of a sequence of contract declarations
- Each contract is abstractly viewed as a sequence of one or more method definitions
- Storage private to a contract, denoted by the keyword global
- Since T is generic, we lose no generality with a single variable
- Regular if-then-else statements
- goto a given line
- Assigns a non-deterministic value
- Check of truth value of predicates
- Blocks until the supplied expression becomes true
- call() invocations (send with argument)
-
$\langle \mathcal{T} ,\sigma \rangle$ - The block$B$ being currently mined -
$\mathcal{T}$ - The completed transactions that are not committed -
$\sigma$ - The global state of the system after executing$\mathcal{T}$ -
$BC$ - The list of commited blocks
-
$id$ - Identifier of the contract -
$g$ - Valuation of global variable
-
$\ell \in Vals$ - The valuation of the method local variables$l$ -
$M$ - The code of the contract with identifier id -
$pc$ - The program counter -
$v:\langle i,o \rangle$ - Auxiliary memory for storing input and output
-
$c:=\langle \gamma, \sigma \rangle$ - The configuration, captures the state of the transaction -
$\rightsquigarrow$ - Small step operation -
$\to$ - Transaction relation for globals and blockchain state -
$\leftarrow$ - Assignment
<Subject> msg.sender </Subject>
<Object> a.seller </Object>
<Operation trigger="pre"> placeBid </Operation>
<Condition> a.seller != msg.sender </Condition>
<Result> True </Result>function placeBid(uint auctionId){
Auction a = auctions[auctionId];
if (a.currentBid >= msg.value)
throw;
uint bidIdx = a.bids.length++;
Bid b = a.bids[bidIdx];
b.bidder = msg.sender;
b.amount = msg.value;
// ...
BidPlaced(auctionId, b.bidder, b.amount);
return true;
}-
$PVars$ - The set of program variables -
$Func$ - The set of function names in a contract -
$Expr$ - The set of conditional expressions
-
Policy specification:
$\langle Sub, Obj, Op, Cond, Res, \rangle$ -
$Sub \in PVar$ - The set of source variables (one or more) that need to be tracked -
$Obj \in PVar$ -
$Op:=\langle f,trig\rangle, f\in Func, trig\in {pre, post}$ -
$Cond \in Expr$ -
$Res \in { T, F}$
-
-
Policy specification:
$\langle Sub, Obj, Op, Cond, Res, \rangle$ -
$Sub \in PVar$ -
$Obj \in PVar$ - The set of variables representing entities with which the subject interacts -
$Op:=\langle f,trig\rangle, f\in Func, trig\in {pre, post}$ -
$Cond \in Expr$ -
$Res \in { T, F}$
-
-
Policy specification:
$\langle Sub, Obj, Op, Cond, Res, \rangle$ -
$Sub \in PVar$ -
$Obj \in PVar$ -
$Op:=\langle f,trig\rangle, f\in Func, trig\in {pre, post}$ - The set of side-affecting invocations that capture the effects of interaction between the subject and the object -
$Cond \in Expr$ -
$Res \in { T, F}$
-
-
Policy specification:
$\langle Sub, Obj, Op, Cond, Res, \rangle$ -
$Sub \in PVar$ -
$Obj \in PVar$ -
$Op:=\langle f,trig\rangle, f\in Func, trig\in {pre, post}$ -
$Cond \in Expr$ - The set of predicates that govern this interaction leading to the operation -
$Res \in { T, F}$
-
-
Policy specification:
$\langle Sub, Obj, Op, Cond, Res, \rangle$ -
$Sub \in PVar$ -
$Obj \in PVar$ -
$Op:=\langle f,trig\rangle, f\in Func, trig\in {pre, post}$ -
$Cond \in Expr$ -
$Res \in { T, F}$ - Indicates whether the interaction between the subject and operation as governed by the predicates is permitted or constitutes a violation
-
- The Policy builder:
$500$ lines of code - The translator from solidity to LLVM:
$3000$ lines of code - The code was written on C++ using the Abstract Syntax Tree (AST) derived from the Solidity compiler
solc - Verifier: Verifiers that are already work with LLVM like
SMACK,Seahorn
function transfer() {
msg.sender.send(msg.value);
balance = balance - msg.value;
}<Subject> msg.value </Subject>
<Object> msg.sender </Object>
<Operation trigger="pre"> send </Operation>
<Condition> msg.value <= balance </Condition>
<Result> True </Result>havoc value
havoc balance
B@δ() {
assert(value <= balance)
post B'@δ()
balance = balance - value
}define void @transfer() {
entry:
% value = getelementptr %msgRecord* @msg, i32 0, i32 4
%0 = load i256* % value
%1 = load i256* @balance
%2 = icmp ule i256 %0, %1
br i1 %2, label %"75", label %"74"
"74":
call void @ VERIFIER error()
br label %"75"
"75":
% sender = getelementptr %msgRecord* @msg, i32 0, i32 2
%3 = load i160* % sender
%4 = call i1 @send(i160 %3, i256 %0)
%5 = sub i256 %1, %0
store i256 %5, i256* @balance
ret void
}
define void @main() {
entry:
%0 = call i256 @ _VERIFIER_NONDET ( )
store 1256 %0, 1256* @balance
//...
}define void @transfer() {
entry:
% value = getelementptr %msgRecord* @msg, i32 0, i32 4
%0 = load i256* % value // Load msg.value into %0
%1 = load i256* @balance // Load balance into %1
%2 = icmp ule i256 %0, %1 // Compare %0 and %1 (%2 = 1 if %0 <= %1)
br i1 %2, label %"75", label %"74" // Branch based on %2
"74": // An assert failure is modeled as a call to the verifier’s error function
call void @ VERIFIER error()
function
br label %"75"
"75": // If %2 is 1 (i.e., value <= balance)
% sender = getelementptr %msgRecord* @msg, i32 0, i32 2
%3 = load i160* % sender
%4 = call i1 @send(i160 %3, i256 %0) // Call send
%5 = sub i256 %1, %0 // balance -= value
store i256 %5, i256* @balance // Store updated balance
ret void
}
define void @main() {
entry: // Globals are automatically havoc-ed to explore the entire data domain
%0 = call i256 @ _VERIFIER_NONDET ( )
store 1256 %0, 1256* @balance
// ...
}contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
// ...
}contract AttackerContract {
function () {
Wallet wallet;
wallet.withdrawBalance();
}
}contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
// ...
}contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance2() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
assert(false);
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
withdrawBalance2();
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
}contract Wallet {
mapping(address => uint) private userBalances;
function withdrawBalance2() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
assert(false); // Now it's unreachable
msg.sender.call(userBalances[msg.sender]);
userBalances[msg.sender] = 0;
}
}
function withdrawBalance() {
uint amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
userBalances[msg.sender] = 0; // The safe version :)
withdrawBalance2();
msg.sender.call(userBalances[msg.sender]);
}
}
}// Globals ...
prizePaidOut = False;
if (gameHasEnded && !prizePaidOut) {
winner.send(1000); // May fail, thus the Ether is lost forever :(
prizePaidOut = True;
}// Globals ...
prizePaidOut = False;
checkSend = True;
if (gameHasEnded && !prizePaidOut) {
checkSend &= winner.send(1000); // False if send fails
assert(checkSend);
prizePaidOut = True;
}// Globals ...
prizePaidOut = False;
checkSend = True;
if (gameHasEnded && !prizePaidOut) {
checkSend &= winner.send(1000); // False if send fails
assert(checkSend);
prizePaidOut = True;
}- Initialize a global variable
checkSendtotrue - Take logical AND of
checkSendand the result of eachsend - For every write of a global variable, assert that
checkSendistrue
// Globals ...
investors = [ ... ];
for (uint i=0; i < investors.length; i++) {
if (investors[i].invested == min investment) {
payout = investors[i].payout;
if (!(investors[i].address.send(payout)))
throw;
investors[i] = newInvestor;
}
}// Globals ...
investors = [ ... ];
checkSend = True;
for (uint i=0; i < investors.length; i++) {
if (investors[i].invested == min investment) {
payout = investors[i].payout;
if (!(checkSend &= investors[i].address.send(payout)))
assert(checkSend);
throw;
investors[i] = newInvestor;
}
}- Same as unchecked send, but assert that
checkSendistruebeforethrow's - Indicates a possibility of reverting the transaction due to control flow reaching a
throwon a failedsend
- Fairness properties involving mathematical formulae are harder to check
- ZEUS depends on the user to give appropriate policy
- Zeus is not faithful exactly to Solidity syntax
- Does not explicitly account for runtime EVM parameters such as gas
-
throwandselfdestructare modeled as program exit
- Zeus does not analyze contracts with an assembly block
- Only
$45$ out of$22,493$ contracts in the data set use it
- Only
- Zeus does not support virtual functions in contract hierarchy (i.e.
super)- Only
$23$ out of$22,493$ contracts in the data set use it
- Only
- 94.6% of 22.4K contracts are vulnerable
- ZEUS is sound (zero false negative)
- Low false positive rate
- ZEUS is fast (less than 1 min to verify 97% of the contracts)





