Transaction Hash:
Block:
11417657 at Dec-09-2020 08:44:41 AM +UTC
Transaction Fee:
0.0112672644 ETH
$21.11
Gas Used:
118,853 Gas / 94.8 Gwei
Emitted Events:
96 |
Dai.Transfer( src=AdExCore, dst=[Receiver] 0x55be297522aae1e282c34b907a1c9d086b5ae613, wad=25179285000000000000 )
|
97 |
AdExCore.LogChannelWithdraw( channelId=F0642215A69FFEC7A41659C2D1810E9718575AB3B1AEF4C37E78B37AF5DA92A1, amount=25179285000000000000 )
|
Account State Difference:
Address | Before | After | State Difference | ||
---|---|---|---|---|---|
0x333420fC...012177d2b | |||||
0x6B175474...495271d0F | |||||
0x942f9CE5...680230348 | (Ambire Wallet: Deployer) |
2.128984748434487019 Eth
Nonce: 11892
|
2.117717484034487019 Eth
Nonce: 11893
| 0.0112672644 | |
0xEA674fdD...16B898ec8
Miner
| (Ethermine) | 627.245472888459932942 Eth | 627.256740152859932942 Eth | 0.0112672644 |
Execution Trace
0x55be297522aae1e282c34b907a1c9d086b5ae613.5dc221e3( )
Identity.executeRoutines( auth=[{name:relayer, type:address, order:1, indexed:false, value:0x942f9CE5D9a33a82F88D233AEb3292E680230348, valueString:0x942f9CE5D9a33a82F88D233AEb3292E680230348}, {name:outpace, type:address, order:2, indexed:false, value:0x333420fC6A897356E69b62417cd17fF012177d2b, valueString:0x333420fC6A897356E69b62417cd17fF012177d2b}, {name:validUntil, type:uint256, order:3, indexed:false, value:10648454444, valueString:10648454444}, {name:feeTokenAddr, type:address, order:4, indexed:false, value:0x6B175474E89094C44Da98b954EedeAC495271d0F, valueString:0x6B175474E89094C44Da98b954EedeAC495271d0F}, {name:weeklyFeeAmount, type:uint256, order:5, indexed:false, value:0, valueString:0}], operations= )
AdExCore.ed66d857( )
-
Null: 0x000...001.1bca7583( )
-
Null: 0x000...001.1bca7583( )
-
Dai.transfer( dst=0x55be297522AAe1E282C34B907a1c9D086b5aE613, wad=25179285000000000000 ) => ( True )
-
File 1 of 3: AdExCore
File 2 of 3: Dai
File 3 of 3: Identity
pragma solidity ^0.5.6; pragma experimental ABIEncoderV2; library SafeMath { function mul(uint a, uint b) internal pure returns (uint) { uint c = a * b; assert(a == 0 || c / a == b); return c; } function div(uint a, uint b) internal pure returns (uint) { assert(b > 0); uint c = a / b; assert(a == b * c + a % b); return c; } function sub(uint a, uint b) internal pure returns (uint) { assert(b <= a); return a - b; } function add(uint a, uint b) internal pure returns (uint) { uint c = a + b; assert(c >= a); return c; } function max64(uint64 a, uint64 b) internal pure returns (uint64) { return a >= b ? a : b; } function min64(uint64 a, uint64 b) internal pure returns (uint64) { return a < b ? a : b; } function max256(uint a, uint b) internal pure returns (uint) { return a >= b ? a : b; } function min256(uint a, uint b) internal pure returns (uint) { return a < b ? a : b; } } interface GeneralERC20 { function transfer(address to, uint256 value) external; function transferFrom(address from, address to, uint256 value) external; function approve(address spender, uint256 value) external; function balanceOf(address spender) external view returns (uint); } library SafeERC20 { function checkSuccess() private pure returns (bool) { uint256 returnValue = 0; assembly { // check number of bytes returned from last function call switch returndatasize // no bytes returned: assume success case 0x0 { returnValue := 1 } // 32 bytes returned: check if non-zero case 0x20 { // copy 32 bytes into scratch space returndatacopy(0x0, 0x0, 0x20) // load those bytes into returnValue returnValue := mload(0x0) } // not sure what was returned: don't mark as success default { } } return returnValue != 0; } function transfer(address token, address to, uint256 amount) internal { GeneralERC20(token).transfer(to, amount); require(checkSuccess()); } function transferFrom(address token, address from, address to, uint256 amount) internal { GeneralERC20(token).transferFrom(from, to, amount); require(checkSuccess()); } function approve(address token, address spender, uint256 amount) internal { GeneralERC20(token).approve(spender, amount); require(checkSuccess()); } } library MerkleProof { function isContained(bytes32 valueHash, bytes32[] memory proof, bytes32 root) internal pure returns (bool) { bytes32 cursor = valueHash; for (uint256 i = 0; i < proof.length; i++) { if (cursor < proof[i]) { cursor = keccak256(abi.encodePacked(cursor, proof[i])); } else { cursor = keccak256(abi.encodePacked(proof[i], cursor)); } } return cursor == root; } } library SignatureValidator { enum SignatureMode { NO_SIG, EIP712, GETH, TREZOR, ADEX } function recoverAddr(bytes32 hash, bytes32[3] memory signature) internal pure returns (address) { SignatureMode mode = SignatureMode(uint8(signature[0][0])); if (mode == SignatureMode.NO_SIG) { return address(0x0); } uint8 v = uint8(signature[0][1]); if (mode == SignatureMode.GETH) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); } else if (mode == SignatureMode.TREZOR) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n\x20", hash)); } else if (mode == SignatureMode.ADEX) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n108By signing this message, you acknowledge signing an AdEx bid with the hash:\n", hash)); } return ecrecover(hash, v, signature[1], signature[2]); } /// @dev Validates that a hash was signed by a specified signer. /// @param hash Hash which was signed. /// @param signer Address of the signer. /// @param signature ECDSA signature along with the mode [{mode}{v}, {r}, {s}] /// @return Returns whether signature is from a specified user. function isValidSignature(bytes32 hash, address signer, bytes32[3] memory signature) internal pure returns (bool) { return recoverAddr(hash, signature) == signer; } } library ChannelLibrary { uint constant MAX_VALIDITY = 365 days; // Both numbers are inclusive uint constant MIN_VALIDATOR_COUNT = 2; // This is an arbitrary number, but we impose this limit to restrict on-chain load; also to ensure the *3 operation is safe uint constant MAX_VALIDATOR_COUNT = 25; enum State { Unknown, Active, Expired } struct Channel { address creator; address tokenAddr; uint tokenAmount; uint validUntil; address[] validators; // finally, arbitrary bytes32 that allows to... @TODO document that this acts as a nonce bytes32 spec; } function hash(Channel memory channel) internal view returns (bytes32) { // In this version of solidity, we can no longer keccak256() directly return keccak256(abi.encode( address(this), channel.creator, channel.tokenAddr, channel.tokenAmount, channel.validUntil, channel.validators, channel.spec )); } function isValid(Channel memory channel, uint currentTime) internal pure returns (bool) { // NOTE: validators[] can be sybil'd by passing the same addr a few times // this does not matter since you can sybil validators[] anyway, and that is mitigated off-chain if (channel.validators.length < MIN_VALIDATOR_COUNT) { return false; } if (channel.validators.length > MAX_VALIDATOR_COUNT) { return false; } if (channel.validUntil < currentTime) { return false; } if (channel.validUntil > currentTime + MAX_VALIDITY) { return false; } return true; } function isSignedBySupermajority(Channel memory channel, bytes32 toSign, bytes32[3][] memory signatures) internal pure returns (bool) { // NOTE: each element of signatures[] must signed by the elem with the same index in validators[] // In case someone didn't sign, pass SignatureMode.NO_SIG if (signatures.length != channel.validators.length) { return false; } uint signs = 0; for (uint i=0; i<signatures.length; i++) { // NOTE: if a validator has not signed, you can just use SignatureMode.NO_SIG if (SignatureValidator.isValidSignature(toSign, channel.validators[i], signatures[i])) { signs++; } } return signs*3 >= channel.validators.length*2; } } // AUDIT: Things we should look for // 1) every time we check the state, the function should either revert or change the state // 2) state transition: channelOpen locks up tokens, then all of the tokens can be withdrawn on channelExpiredWithdraw, except how many were withdrawn using channelWithdraw // 3) external calls (everything using SafeERC20) should be at the end // 4) channel can always be 100% drained with Withdraw/ExpiredWithdraw contract AdExCore { using SafeMath for uint; using ChannelLibrary for ChannelLibrary.Channel; // channelId => channelState mapping (bytes32 => ChannelLibrary.State) public states; // withdrawn per channel (channelId => uint) mapping (bytes32 => uint) public withdrawn; // withdrawn per channel user (channelId => (account => uint)) mapping (bytes32 => mapping (address => uint)) public withdrawnPerUser; // Events event LogChannelOpen(bytes32 indexed channelId); event LogChannelWithdrawExpired(bytes32 indexed channelId, uint amount); event LogChannelWithdraw(bytes32 indexed channelId, uint amount); // All functions are public function channelOpen(ChannelLibrary.Channel memory channel) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Unknown, "INVALID_STATE"); require(msg.sender == channel.creator, "INVALID_CREATOR"); require(channel.isValid(now), "INVALID_CHANNEL"); states[channelId] = ChannelLibrary.State.Active; SafeERC20.transferFrom(channel.tokenAddr, msg.sender, address(this), channel.tokenAmount); emit LogChannelOpen(channelId); } function channelWithdrawExpired(ChannelLibrary.Channel memory channel) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Active, "INVALID_STATE"); require(now > channel.validUntil, "NOT_EXPIRED"); require(msg.sender == channel.creator, "INVALID_CREATOR"); uint toWithdraw = channel.tokenAmount.sub(withdrawn[channelId]); // NOTE: we will not update withdrawn, since a WithdrawExpired does not count towards normal withdrawals states[channelId] = ChannelLibrary.State.Expired; SafeERC20.transfer(channel.tokenAddr, msg.sender, toWithdraw); emit LogChannelWithdrawExpired(channelId, toWithdraw); } function channelWithdraw(ChannelLibrary.Channel memory channel, bytes32 stateRoot, bytes32[3][] memory signatures, bytes32[] memory proof, uint amountInTree) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Active, "INVALID_STATE"); require(now <= channel.validUntil, "EXPIRED"); bytes32 hashToSign = keccak256(abi.encode(channelId, stateRoot)); require(channel.isSignedBySupermajority(hashToSign, signatures), "NOT_SIGNED_BY_VALIDATORS"); bytes32 balanceLeaf = keccak256(abi.encode(msg.sender, amountInTree)); require(MerkleProof.isContained(balanceLeaf, proof, stateRoot), "BALANCELEAF_NOT_FOUND"); // The user can withdraw their constantly increasing balance at any time (essentially prevent users from double spending) uint toWithdraw = amountInTree.sub(withdrawnPerUser[channelId][msg.sender]); withdrawnPerUser[channelId][msg.sender] = amountInTree; // Ensure that it's not possible to withdraw more than the channel deposit (e.g. malicious validators sign such a state) withdrawn[channelId] = withdrawn[channelId].add(toWithdraw); require(withdrawn[channelId] <= channel.tokenAmount, "WITHDRAWING_MORE_THAN_CHANNEL"); SafeERC20.transfer(channel.tokenAddr, msg.sender, toWithdraw); emit LogChannelWithdraw(channelId, toWithdraw); } }
File 2 of 3: Dai
// hevm: flattened sources of /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/dai.sol pragma solidity =0.5.12; ////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/lib.sol // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. /* pragma solidity 0.5.12; */ contract LibNote { event LogNote( bytes4 indexed sig, address indexed usr, bytes32 indexed arg1, bytes32 indexed arg2, bytes data ) anonymous; modifier note { _; assembly { // log an 'anonymous' event with a constant 6 words of calldata // and four indexed topics: selector, caller, arg1 and arg2 let mark := msize // end of memory ensures zero mstore(0x40, add(mark, 288)) // update free memory pointer mstore(mark, 0x20) // bytes type data offset mstore(add(mark, 0x20), 224) // bytes size (padded) calldatacopy(add(mark, 0x40), 0, 224) // bytes payload log4(mark, 288, // calldata shl(224, shr(224, calldataload(0))), // msg.sig caller, // msg.sender calldataload(4), // arg1 calldataload(36) // arg2 ) } } } ////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/dai.sol // Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. /* pragma solidity 0.5.12; */ /* import "./lib.sol"; */ contract Dai is LibNote { // --- Auth --- mapping (address => uint) public wards; function rely(address guy) external note auth { wards[guy] = 1; } function deny(address guy) external note auth { wards[guy] = 0; } modifier auth { require(wards[msg.sender] == 1, "Dai/not-authorized"); _; } // --- ERC20 Data --- string public constant name = "Dai Stablecoin"; string public constant symbol = "DAI"; string public constant version = "1"; uint8 public constant decimals = 18; uint256 public totalSupply; mapping (address => uint) public balanceOf; mapping (address => mapping (address => uint)) public allowance; mapping (address => uint) public nonces; event Approval(address indexed src, address indexed guy, uint wad); event Transfer(address indexed src, address indexed dst, uint wad); // --- Math --- function add(uint x, uint y) internal pure returns (uint z) { require((z = x + y) >= x); } function sub(uint x, uint y) internal pure returns (uint z) { require((z = x - y) <= x); } // --- EIP712 niceties --- bytes32 public DOMAIN_SEPARATOR; // bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"); bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb; constructor(uint256 chainId_) public { wards[msg.sender] = 1; DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(name)), keccak256(bytes(version)), chainId_, address(this) )); } // --- Token --- function transfer(address dst, uint wad) external returns (bool) { return transferFrom(msg.sender, dst, wad); } function transferFrom(address src, address dst, uint wad) public returns (bool) { require(balanceOf[src] >= wad, "Dai/insufficient-balance"); if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { require(allowance[src][msg.sender] >= wad, "Dai/insufficient-allowance"); allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); } balanceOf[src] = sub(balanceOf[src], wad); balanceOf[dst] = add(balanceOf[dst], wad); emit Transfer(src, dst, wad); return true; } function mint(address usr, uint wad) external auth { balanceOf[usr] = add(balanceOf[usr], wad); totalSupply = add(totalSupply, wad); emit Transfer(address(0), usr, wad); } function burn(address usr, uint wad) external { require(balanceOf[usr] >= wad, "Dai/insufficient-balance"); if (usr != msg.sender && allowance[usr][msg.sender] != uint(-1)) { require(allowance[usr][msg.sender] >= wad, "Dai/insufficient-allowance"); allowance[usr][msg.sender] = sub(allowance[usr][msg.sender], wad); } balanceOf[usr] = sub(balanceOf[usr], wad); totalSupply = sub(totalSupply, wad); emit Transfer(usr, address(0), wad); } function approve(address usr, uint wad) external returns (bool) { allowance[msg.sender][usr] = wad; emit Approval(msg.sender, usr, wad); return true; } // --- Alias --- function push(address usr, uint wad) external { transferFrom(msg.sender, usr, wad); } function pull(address usr, uint wad) external { transferFrom(usr, msg.sender, wad); } function move(address src, address dst, uint wad) external { transferFrom(src, dst, wad); } // --- Approve by signature --- function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external { bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, holder, spender, nonce, expiry, allowed)) )); require(holder != address(0), "Dai/invalid-address-0"); require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit"); require(expiry == 0 || now <= expiry, "Dai/permit-expired"); require(nonce == nonces[holder]++, "Dai/invalid-nonce"); uint wad = allowed ? uint(-1) : 0; allowance[holder][spender] = wad; emit Approval(holder, spender, wad); } }
File 3 of 3: Identity
/** *Submitted for verification at Etherscan.io on 2019-11-23 */ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; library SafeMath { function mul(uint a, uint b) internal pure returns (uint) { uint c = a * b; require(a == 0 || c / a == b); return c; } function div(uint a, uint b) internal pure returns (uint) { require(b > 0); uint c = a / b; require(a == b * c + a % b); return c; } function sub(uint a, uint b) internal pure returns (uint) { require(b <= a); return a - b; } function add(uint a, uint b) internal pure returns (uint) { uint c = a + b; require(c >= a); return c; } function max64(uint64 a, uint64 b) internal pure returns (uint64) { return a >= b ? a : b; } function min64(uint64 a, uint64 b) internal pure returns (uint64) { return a < b ? a : b; } function max256(uint a, uint b) internal pure returns (uint) { return a >= b ? a : b; } function min256(uint a, uint b) internal pure returns (uint) { return a < b ? a : b; } } interface GeneralERC20 { function transfer(address to, uint256 value) external; function transferFrom(address from, address to, uint256 value) external; function approve(address spender, uint256 value) external; function balanceOf(address spender) external view returns (uint); function allowance(address owner, address spender) external view returns (uint); } library SafeERC20 { function checkSuccess() private pure returns (bool) { uint256 returnValue = 0; assembly { // check number of bytes returned from last function call switch returndatasize // no bytes returned: assume success case 0x0 { returnValue := 1 } // 32 bytes returned: check if non-zero case 0x20 { // copy 32 bytes into scratch space returndatacopy(0x0, 0x0, 0x20) // load those bytes into returnValue returnValue := mload(0x0) } // not sure what was returned: don't mark as success default { } } return returnValue != 0; } function transfer(address token, address to, uint256 amount) internal { GeneralERC20(token).transfer(to, amount); require(checkSuccess()); } function transferFrom(address token, address from, address to, uint256 amount) internal { GeneralERC20(token).transferFrom(from, to, amount); require(checkSuccess()); } function approve(address token, address spender, uint256 amount) internal { GeneralERC20(token).approve(spender, amount); require(checkSuccess()); } } library SignatureValidator { enum SignatureMode { NO_SIG, EIP712, GETH, TREZOR, ADEX } function recoverAddr(bytes32 hash, bytes32[3] memory signature) internal pure returns (address) { SignatureMode mode = SignatureMode(uint8(signature[0][0])); if (mode == SignatureMode.NO_SIG) { return address(0x0); } uint8 v = uint8(signature[0][1]); if (mode == SignatureMode.GETH) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); } else if (mode == SignatureMode.TREZOR) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n\x20", hash)); } else if (mode == SignatureMode.ADEX) { hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n108By signing this message, you acknowledge signing an AdEx bid with the hash:\n", hash)); } return ecrecover(hash, v, signature[1], signature[2]); } /// @dev Validates that a hash was signed by a specified signer. /// @param hash Hash which was signed. /// @param signer Address of the signer. /// @param signature ECDSA signature along with the mode [{mode}{v}, {r}, {s}] /// @return Returns whether signature is from a specified user. function isValidSignature(bytes32 hash, address signer, bytes32[3] memory signature) internal pure returns (bool) { return recoverAddr(hash, signature) == signer; } } library ChannelLibrary { uint constant MAX_VALIDITY = 365 days; // Both numbers are inclusive uint constant MIN_VALIDATOR_COUNT = 2; // This is an arbitrary number, but we impose this limit to restrict on-chain load; also to ensure the *3 operation is safe uint constant MAX_VALIDATOR_COUNT = 25; enum State { Unknown, Active, Expired } struct Channel { address creator; address tokenAddr; uint tokenAmount; uint validUntil; address[] validators; // finally, arbitrary bytes32 that allows to... @TODO document that this acts as a nonce bytes32 spec; } function hash(Channel memory channel) internal view returns (bytes32) { // In this version of solidity, we can no longer keccak256() directly return keccak256(abi.encode( address(this), channel.creator, channel.tokenAddr, channel.tokenAmount, channel.validUntil, channel.validators, channel.spec )); } function isValid(Channel memory channel, uint currentTime) internal pure returns (bool) { // NOTE: validators[] can be sybil'd by passing the same addr a few times // this does not matter since you can sybil validators[] anyway, and that is mitigated off-chain if (channel.validators.length < MIN_VALIDATOR_COUNT) { return false; } if (channel.validators.length > MAX_VALIDATOR_COUNT) { return false; } if (channel.validUntil < currentTime) { return false; } if (channel.validUntil > (currentTime + MAX_VALIDITY)) { return false; } return true; } function isSignedBySupermajority(Channel memory channel, bytes32 toSign, bytes32[3][] memory signatures) internal pure returns (bool) { // NOTE: each element of signatures[] must signed by the elem with the same index in validators[] // In case someone didn't sign, pass SignatureMode.NO_SIG if (signatures.length != channel.validators.length) { return false; } uint signs = 0; uint sigLen = signatures.length; for (uint i=0; i<sigLen; i++) { // NOTE: if a validator has not signed, you can just use SignatureMode.NO_SIG if (SignatureValidator.isValidSignature(toSign, channel.validators[i], signatures[i])) { signs++; } else if (i == 0) { // The 0th signature is always from the leading validator, so it doesn't make sense for other sigs to exist if this one does not return false; } } return signs*3 >= channel.validators.length*2; } } library MerkleProof { function isContained(bytes32 valueHash, bytes32[] memory proof, bytes32 root) internal pure returns (bool) { bytes32 cursor = valueHash; uint256 proofLen = proof.length; for (uint256 i = 0; i < proofLen; i++) { if (cursor < proof[i]) { cursor = keccak256(abi.encodePacked(cursor, proof[i])); } else { cursor = keccak256(abi.encodePacked(proof[i], cursor)); } } return cursor == root; } } // AUDIT: Things we should look for // 1) every time we check the state, the function should either revert or change the state // 2) state transition: channelOpen locks up tokens, then all of the tokens can be withdrawn on channelExpiredWithdraw, except how many were withdrawn using channelWithdraw // 3) external calls (everything using SafeERC20) should be at the end // 4) channel can always be 100% drained with Withdraw/ExpiredWithdraw contract AdExCore { using SafeMath for uint; using ChannelLibrary for ChannelLibrary.Channel; // channelId => channelState mapping (bytes32 => ChannelLibrary.State) public states; // withdrawn per channel (channelId => uint) mapping (bytes32 => uint) public withdrawn; // withdrawn per channel user (channelId => (account => uint)) mapping (bytes32 => mapping (address => uint)) public withdrawnPerUser; // Events event LogChannelOpen(bytes32 indexed channelId); event LogChannelWithdrawExpired(bytes32 indexed channelId, uint amount); event LogChannelWithdraw(bytes32 indexed channelId, uint amount); // All functions are public function channelOpen(ChannelLibrary.Channel memory channel) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Unknown, "INVALID_STATE"); require(msg.sender == channel.creator, "INVALID_CREATOR"); require(channel.isValid(now), "INVALID_CHANNEL"); states[channelId] = ChannelLibrary.State.Active; SafeERC20.transferFrom(channel.tokenAddr, msg.sender, address(this), channel.tokenAmount); emit LogChannelOpen(channelId); } function channelWithdrawExpired(ChannelLibrary.Channel memory channel) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Active, "INVALID_STATE"); require(now > channel.validUntil, "NOT_EXPIRED"); require(msg.sender == channel.creator, "INVALID_CREATOR"); uint toWithdraw = channel.tokenAmount.sub(withdrawn[channelId]); // NOTE: we will not update withdrawn, since a WithdrawExpired does not count towards normal withdrawals states[channelId] = ChannelLibrary.State.Expired; SafeERC20.transfer(channel.tokenAddr, msg.sender, toWithdraw); emit LogChannelWithdrawExpired(channelId, toWithdraw); } function channelWithdraw(ChannelLibrary.Channel memory channel, bytes32 stateRoot, bytes32[3][] memory signatures, bytes32[] memory proof, uint amountInTree) public { bytes32 channelId = channel.hash(); require(states[channelId] == ChannelLibrary.State.Active, "INVALID_STATE"); require(now <= channel.validUntil, "EXPIRED"); bytes32 hashToSign = keccak256(abi.encode(channelId, stateRoot)); require(channel.isSignedBySupermajority(hashToSign, signatures), "NOT_SIGNED_BY_VALIDATORS"); bytes32 balanceLeaf = keccak256(abi.encode(msg.sender, amountInTree)); require(MerkleProof.isContained(balanceLeaf, proof, stateRoot), "BALANCELEAF_NOT_FOUND"); // The user can withdraw their constantly increasing balance at any time (essentially prevent users from double spending) uint toWithdraw = amountInTree.sub(withdrawnPerUser[channelId][msg.sender]); withdrawnPerUser[channelId][msg.sender] = amountInTree; // Ensure that it's not possible to withdraw more than the channel deposit (e.g. malicious validators sign such a state) withdrawn[channelId] = withdrawn[channelId].add(toWithdraw); require(withdrawn[channelId] <= channel.tokenAmount, "WITHDRAWING_MORE_THAN_CHANNEL"); SafeERC20.transfer(channel.tokenAddr, msg.sender, toWithdraw); emit LogChannelWithdraw(channelId, toWithdraw); } } contract Identity { using SafeMath for uint; // Storage // WARNING: be careful when modifying this // privileges and routineAuthorizations must always be 0th and 1th thing in storage, // because of the proxies we generate that delegatecall into this contract (which assume storage slot 0 and 1) mapping (address => uint8) public privileges; // Routine authorizations mapping (bytes32 => bool) public routineAuthorizations; // The next allowed nonce uint public nonce = 0; // Routine operations are authorized at once for a period, fee is paid once mapping (bytes32 => uint256) public routinePaidFees; // Constants bytes4 private constant CHANNEL_WITHDRAW_SELECTOR = bytes4(keccak256('channelWithdraw((address,address,uint256,uint256,address[],bytes32),bytes32,bytes32[3][],bytes32[],uint256)')); bytes4 private constant CHANNEL_WITHDRAW_EXPIRED_SELECTOR = bytes4(keccak256('channelWithdrawExpired((address,address,uint256,uint256,address[],bytes32))')); enum PrivilegeLevel { None, Routines, Transactions } enum RoutineOp { ChannelWithdraw, ChannelWithdrawExpired } // Events event LogPrivilegeChanged(address indexed addr, uint8 privLevel); event LogRoutineAuth(bytes32 hash, bool authorized); // Transaction structure // Those can be executed by keys with >= PrivilegeLevel.Transactions // Even though the contract cannot receive ETH, we are able to send ETH (.value), cause ETH might've been sent to the contract address before it's deployed struct Transaction { // replay protection address identityContract; uint nonce; // tx fee, in tokens address feeTokenAddr; uint feeAmount; // all the regular txn data address to; uint value; bytes data; } // RoutineAuthorizations allow the user to authorize (via keys >= PrivilegeLevel.Routines) a relayer to do any number of routines // those routines are safe: e.g. sweeping channels (withdrawing off-chain balances to the identity) // while the fee will be paid only ONCE per auth per period (1 week), the authorization can be used until validUntil // while the routines are safe, there is some level of implied trust as the relayer may run executeRoutines without any routines to claim the fee struct RoutineAuthorization { address relayer; address outpace; uint validUntil; address feeTokenAddr; uint weeklyFeeAmount; } struct RoutineOperation { RoutineOp mode; bytes data; } constructor(address[] memory addrs, uint8[] memory privLevels) public { uint len = privLevels.length; for (uint i=0; i<len; i++) { privileges[addrs[i]] = privLevels[i]; emit LogPrivilegeChanged(addrs[i], privLevels[i]); } } function setAddrPrivilege(address addr, uint8 privLevel) external { require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL'); privileges[addr] = privLevel; emit LogPrivilegeChanged(addr, privLevel); } function setRoutineAuth(bytes32 hash, bool authorized) external { require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL'); routineAuthorizations[hash] = authorized; emit LogRoutineAuth(hash, authorized); } function channelOpen(address coreAddr, ChannelLibrary.Channel memory channel) public { require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL'); if (GeneralERC20(channel.tokenAddr).allowance(address(this), coreAddr) > 0) { SafeERC20.approve(channel.tokenAddr, coreAddr, 0); } SafeERC20.approve(channel.tokenAddr, coreAddr, channel.tokenAmount); AdExCore(coreAddr).channelOpen(channel); } function execute(Transaction[] memory txns, bytes32[3][] memory signatures) public { require(txns.length > 0, 'MUST_PASS_TX'); address feeTokenAddr = txns[0].feeTokenAddr; uint feeAmount = 0; uint len = txns.length; for (uint i=0; i<len; i++) { Transaction memory txn = txns[i]; require(txn.identityContract == address(this), 'TRANSACTION_NOT_FOR_CONTRACT'); require(txn.feeTokenAddr == feeTokenAddr, 'EXECUTE_NEEDS_SINGLE_TOKEN'); require(txn.nonce == nonce, 'WRONG_NONCE'); // If we use the naive abi.encode(txn) and have a field of type `bytes`, // there is a discrepancy between ethereumjs-abi and solidity // if we enter every field individually, in order, there is no discrepancy //bytes32 hash = keccak256(abi.encode(txn)); bytes32 hash = keccak256(abi.encode(txn.identityContract, txn.nonce, txn.feeTokenAddr, txn.feeAmount, txn.to, txn.value, txn.data)); address signer = SignatureValidator.recoverAddr(hash, signatures[i]); require(privileges[signer] >= uint8(PrivilegeLevel.Transactions), 'INSUFFICIENT_PRIVILEGE_TRANSACTION'); nonce = nonce.add(1); feeAmount = feeAmount.add(txn.feeAmount); executeCall(txn.to, txn.value, txn.data); // The actual anti-bricking mechanism - do not allow a signer to drop his own priviledges require(privileges[signer] >= uint8(PrivilegeLevel.Transactions), 'PRIVILEGE_NOT_DOWNGRADED'); } if (feeAmount > 0) { SafeERC20.transfer(feeTokenAddr, msg.sender, feeAmount); } } function executeBySender(Transaction[] memory txns) public { require(privileges[msg.sender] >= uint8(PrivilegeLevel.Transactions), 'INSUFFICIENT_PRIVILEGE_SENDER'); uint len = txns.length; for (uint i=0; i<len; i++) { Transaction memory txn = txns[i]; require(txn.nonce == nonce, 'WRONG_NONCE'); nonce = nonce.add(1); executeCall(txn.to, txn.value, txn.data); } // The actual anti-bricking mechanism - do not allow the sender to drop his own priviledges require(privileges[msg.sender] >= uint8(PrivilegeLevel.Transactions), 'PRIVILEGE_NOT_DOWNGRADED'); } function executeRoutines(RoutineAuthorization memory auth, RoutineOperation[] memory operations) public { require(auth.validUntil >= now, 'AUTHORIZATION_EXPIRED'); bytes32 hash = keccak256(abi.encode(auth)); require(routineAuthorizations[hash], 'NO_AUTHORIZATION'); uint len = operations.length; for (uint i=0; i<len; i++) { RoutineOperation memory op = operations[i]; if (op.mode == RoutineOp.ChannelWithdraw) { // Channel: Withdraw executeCall(auth.outpace, 0, abi.encodePacked(CHANNEL_WITHDRAW_SELECTOR, op.data)); } else if (op.mode == RoutineOp.ChannelWithdrawExpired) { // Channel: Withdraw Expired executeCall(auth.outpace, 0, abi.encodePacked(CHANNEL_WITHDRAW_EXPIRED_SELECTOR, op.data)); } else { revert('INVALID_MODE'); } } if (auth.weeklyFeeAmount > 0 && (now - routinePaidFees[hash]) >= 7 days) { routinePaidFees[hash] = now; SafeERC20.transfer(auth.feeTokenAddr, auth.relayer, auth.weeklyFeeAmount); } } // we shouldn't use address.call(), cause: https://github.com/ethereum/solidity/issues/2884 // copied from https://github.com/uport-project/uport-identity/blob/develop/contracts/Proxy.sol // there's also // https://github.com/gnosis/MultiSigWallet/commit/e1b25e8632ca28e9e9e09c81bd20bf33fdb405ce // https://github.com/austintgriffith/bouncer-proxy/blob/master/BouncerProxy/BouncerProxy.sol // https://github.com/gnosis/safe-contracts/blob/7e2eeb3328bb2ae85c36bc11ea6afc14baeb663c/contracts/base/Executor.sol function executeCall(address to, uint256 value, bytes memory data) internal { assembly { let result := call(gas, to, value, add(data, 0x20), mload(data), 0, 0) switch result case 0 { let size := returndatasize let ptr := mload(0x40) returndatacopy(ptr, 0, size) revert(ptr, size) } default {} } } }