1

I have a contract holding a mapping like this: mapping (address => User) users. How can I get the contract to automatically fire a transaction/event when a certain condition is met? For example, when the user's expiration timestamp reaches a certain value.

  • 1
    You cannot do it from the contract (or from any other contract for that matter). You can do it using an off-chain service which will execute the transaction. In order to enhance transparency and decentralization, you can implement in the contract all the logic which determines when this transaction can be executed. But initiating it can only be performed by an external service (aka Oracle). – goodvibration Jan 21 at 18:39
  • 1
    Thank you very much, @goodvibration, I figured this probably wouldn't be possible – georgebax Jan 21 at 19:14
1

You can do that but there are some subtleties to work out.

It is recommended to use a withdrawal pattern rather than sending a transfer to someone other than msg.sender. There are security concerns talking to more than one untrusted contract at a time, so the safe bet is to just do some accounting rather than intiating a .transfer(). An exception is token contracts you explicitly trust. That should be okay.

Then, you need a data structure that makes it an O(1) (one move) complexity operation to fetch the next work item. For example, if time is the trigger, then a pending queue sorted by trigger time. There are different approaches to that. Below, it uses a self-balancing b-tree to get it done.

Then, you need a way to trigger it.

A simplistic trigger is just relying on an external actor to bang on the contract and check for work to do and pay for the gas. There are ways to avoid that AND ensure that there is no external actor to act as single point of failure.

One way (not the only way) is to use the Amortized Work pattern. In that scheme, anyone who is willing to pay for gas to any mutating function will pay for a little gas to process an item in the queue. You can use a modifier to hook the amortized process into regular functions, or just insert a function call in each case. This approach can even be used to return a correct net value from view functions.

For example:

// in case we are in a view function, this will return the correct result but the changes won't stick. 
// if someone is paying for gas then the changes will stick.

function netBalance(address user) private {
  balance[user] = balance[user] + pendingReceipts[user] - pendingTransfers[user];
  // do something with the pendings
  pendingReceipts[user] = 0;
  pendingTransfers[user] = 0;
  _;
}

function balanceOf(address user) public returns(uint) {
  return netBalance(user);
}

Or, use a modifier on a mutating function to process the queue:

function doSomeUnrelatedThing(address user) public amortizedWork {
  // carry on
}

The code sample below uses a b-tree library to sort pending transactions in timestamp order. It uses the amortization pattern to pull one backlogged item off the queue and do something whenever gas is available. If you want to use the b-tree, be sure to review the scaling guidance.

pragma solidity 0.5.16;

import "./HitchensOrderTree.sol";

contract ProcessAmortization {

    using HitchensOrderStatisticsTreeLib for HitchensOrderStatisticsTreeLib.Tree;

    struct Pending {
        uint amount;
        address user;
        uint validTime;
    }

    mapping(bytes32 => Pending) public pendingStructs; // pendingId => pendingStruct
    HitchensOrderStatisticsTreeLib.Tree pendingSorted; // expiry => pendingId 

    event LogNewPending(address sender, uint validTime, address user, uint amount, bytes32 key);
    event LogCancelledPending(address sender, address user, bytes32 key);
    event LogProcessedPending(address sender, bytes32 key, uint amount, address user, uint validTime);

    modifier amortizedWork {
        // if there is something to do. CONSIDER handling more than one at a time, but mind the gasLimit.
        if(backlog() > 0) {  
            // the lowest time in the sorted list. 
            uint first = next(); 
            // the first pending txn in the group (because multiple txns can have the same pending time)
            bytes32 key = pendingSorted.valueKeyAtIndex(first, 0); 
            // process this one
            Pending storage p = pendingStructs[key]; 
            // send it with p.amount, p.user
            // IT IS RECOMMENDED TO MAKE THIS AN ACCOUNTING FUNCTION RATHER THAN INTERACTING WITH EXTERNAL ACCOUNTS
            // remove it from the queue
            pendingSorted.remove(key, first);
            emit LogProcessedPending(msg.sender, key, p.amount, p.user, p.validTime);
        }
        _;
    }

    function pendingKey(uint validTime, address user, uint amount) public pure returns(bytes32) {
        return keccak256(abi.encodePacked(validTime, user, amount));
    }

    function insertPendingTransfer(uint validTime, address user, uint amount) public amortizedWork {
        bytes32 key = pendingKey(validTime, user, amount); // if you want to allow duplicates, add a nonce
        emit LogNewPending(msg.sender, validTime, user, amount, key);
        pendingSorted.insert(key, validTime); // insert the key into the b-tree, sorted (asc) by validTime
        Pending storage p = pendingStructs[key];
        p.amount = amount;
        p.user = user;
        p.validTime = validTime;
    }

    function cancelPendingTransfer(address user, bytes32 key) public amortizedWork {
        emit LogCancelledPending(msg.sender, user, key);
        Pending storage p = pendingStructs[key];
        pendingSorted.remove(key, p.validTime); // pull the element from the sorted list
        delete pendingStructs[key];
    }

    function next() public view returns(uint nextTime) {
        nextTime = pendingSorted.first();
    }

    function backlog() public view returns(uint count) {
        count = pendingSorted.below(now);
    }

    function whatTimeIsIt() public view returns(uint) {
        return now;
    }

    // it is possible to create all sorts of tree explorers to enumerate the pending transactions queue

}

The OrderStatisticsTree code is over here: https://github.com/rob-Hitchens/OrderStatisticsTree

Hope it helps.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.